DEV Community

ShipRules
ShipRules

Posted on

Your AI agent says "done". Your tests say 3 failing. The Stop hook that settles it

You ask an agent to fix a bug. It edits four files, writes a cheerful summary, and says the task is complete. You run the tests and three of them fail. Sometimes it never ran them at all. Sometimes it ran them, saw red, and wrote "done" anyway.

If you're running several agents in parallel, it's worse: each one reports success on its own branch, and the feature still doesn't work when you put the branches together.

This isn't a model being lazy. An agent's "done" is a self-report, and the agent is grading its own homework. Adding "ALWAYS RUN THE TESTS" to your rules file in capital letters doesn't change that. A check that runs whether the agent wants it to or not does.

This post covers three things:

  1. Where agent rules actually go in Cursor, Claude Code and Codex (and one gotcha that silently unloads them).
  2. What to commit and what to keep local.
  3. How to move the rules that must never break out of prose and into a Stop hook and CI.

1. Where the rules go

Each tool reads a different set of files. Here's the current picture, checked against each tool's docs this week:

Tool Shared project rules Scoped rules Personal / local
Cursor AGENTS.md (root and nested), .cursor/rules/*.mdc .mdc with globs: User Rules (Settings)
Claude Code CLAUDE.md or .claude/CLAUDE.md (can also read AGENTS.md) .claude/rules/*.md with paths: CLAUDE.local.md, ~/.claude/CLAUDE.md
Codex AGENTS.md at each level from the repo root down Nested AGENTS.md / AGENTS.override.md ~/.codex/AGENTS.md

A few details that matter:

  • Cursor only picks up .mdc files in .cursor/rules/. A plain .md file there is ignored. If you want plain markdown, use AGENTS.md.
  • Codex stops reading at 32 KiB of combined instructions by default (project_doc_max_bytes). A sprawling rules file can quietly lose its tail.
  • Claude Code's docs suggest keeping each CLAUDE.md under about 200 lines, and moving anything that only applies to part of the codebase into path-scoped rules.

The setup I use: one AGENTS.md as the source of truth, plus a tiny CLAUDE.md that imports it:

@AGENTS.md

## Claude Code only
- Use plan mode before touching migrations or billing code.
Enter fullscreen mode Exit fullscreen mode

Claude Code expands @path imports at launch, so the AGENTS.md content ends up in context and you only maintain the rules in one place. Codex and Cursor read AGENTS.md directly.

For scoped rules, Cursor and Claude Code use different frontmatter keys for the same idea:

---
description: API route conventions
globs: src/app/api/**/*.ts
alwaysApply: false
---
- Validate every request body with zod before touching the database.
Enter fullscreen mode Exit fullscreen mode
---
paths:
  - "src/app/api/**/*.ts"
---
- Validate every request body with zod before touching the database.
Enter fullscreen mode Exit fullscreen mode

The first is .cursor/rules/api.mdc; the second is .claude/rules/api.md. Scoped rules only load when the agent works on matching files, so your backend rules stop eating context while you edit CSS.

2. Commit the team's rules, keep your habits local

This came up a lot this week: should AGENTS.md be committed? Yes. A simple test for each line in it:

Would a reviewer reject a PR that breaks this rule? If so, commit it. If it's just how you like to work, keep it local.

Commit: AGENTS.md, CLAUDE.md, .cursor/rules/, .claude/rules/, .claude/settings.json (where Claude Code hooks live), .cursor/hooks.json and the hook scripts themselves.

Keep local: "explain your plan before editing", "I prefer pnpm", your sandbox URLs, test accounts, tone preferences. These go in CLAUDE.local.md, .claude/settings.local.json, ~/.claude/CLAUDE.md, ~/.codex/AGENTS.md or Cursor's User Rules.

# .gitignore
CLAUDE.local.md
.claude/settings.local.json
Enter fullscreen mode Exit fullscreen mode

The gotcha: CLAUDE.local.md can switch off your AGENTS.md

By default, Claude Code reads AGENTS.md only when there's no CLAUDE.md in the project. And a CLAUDE.local.md counts as one.

So if your team keeps everything in AGENTS.md with no CLAUDE.md, and you add a personal CLAUDE.local.md, Claude Code reads your local file instead of the team rules. Nobody else sees the problem, because their checkout doesn't have your local file.

Two fixes:

  • Commit a CLAUDE.md that contains @AGENTS.md (the setup above). The import always loads.
  • Or set Project instructions to claude-md-and-agents-md in /config.

Also note that Claude Code doesn't read AGENTS.override.md or AGENTS.local.md. Those are Codex conventions.

3. Prose is a request. Hooks and CI are a gate.

Here's the rule I'd tape to the monitor:

If breaking a rule causes a bug or an incident, it isn't a rule. It's a check.

"Prefer small functions" can stay as prose. "Tests must pass before you say done" can't, because the agent only follows it when it remembers to and agrees with itself that it applies.

You get two layers:

  1. A Stop hook catches it in the session, while the agent can still fix things.
  2. CI with a required status check catches it at merge, whatever agent or human wrote the code.

Claude Code: a Stop hook that won't accept "done" on red

Claude Code fires a Stop event when the agent finishes responding. If a Stop hook exits with code 2, Claude Code blocks the stop and sends the hook's stderr back to the agent as the reason. In other words, the agent gets your failing test output and has to keep working.

.claude/settings.json (committed):

{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          {
            "type": "command",
            "command": "node \"$CLAUDE_PROJECT_DIR\"/.claude/hooks/require-green.mjs",
            "timeout": 300
          }
        ]
      }
    ]
  }
}
Enter fullscreen mode Exit fullscreen mode

Stop doesn't take a matcher, so there isn't one. The default command timeout is 600 seconds; set your own if your suite is slow.

.claude/hooks/require-green.mjs:

#!/usr/bin/env node
// Blocks Claude Code from finishing while the test suite is red.
import { execSync, spawnSync } from "node:child_process";
import { readFileSync } from "node:fs";

const input = JSON.parse(readFileSync(0, "utf8")); // hook JSON arrives on stdin

// Pure Q&A turn with no changes in the working tree? Let it stop.
const changed = execSync("git status --porcelain", { encoding: "utf8" }).trim();
if (!changed) process.exit(0);

const run = spawnSync("npm", ["test", "--silent"], {
  encoding: "utf8",
  shell: process.platform === "win32",
});
if (run.status === 0) process.exit(0);

const tail = `${run.stdout}\n${run.stderr}`.trim().split("\n").slice(-40).join("\n");
process.stderr.write(
  (input.stop_hook_active ? "Still failing after the last attempt.\n" : "") +
    "The test suite is failing, so this task isn't done. " +
    "Fix the code rather than editing or skipping the tests, then run npm test.\n\n" +
    tail + "\n"
);
process.exit(2);
Enter fullscreen mode Exit fullscreen mode

Some notes from the docs:

  • stop_hook_active is true when the agent is already continuing because a Stop hook blocked it. The docs tell you to check it so you don't block forever on something that can't be fixed. The script above uses it only to change the message.
  • There's a built-in cap. After Stop hooks continue the turn 8 times in a row, Claude Code ends the turn anyway. The count resets every time the agent calls a tool. You can change it with CLAUDE_CODE_STOP_HOOK_BLOCK_CAP. If you'd rather give up after a single retry, exit 0 when stop_hook_active is true.
  • Use exit code 2, not 1. Exit 1 is a non-blocking error: the agent stops anyway, and all you get is a notice in the transcript.
  • The git status shortcut keeps the hook from running your whole suite after "what does this function do?". If you often start sessions with uncommitted changes, compare against a stash or a saved hash instead.

The line "fix the code rather than editing or skipping the tests" is there on purpose. When an agent is under pressure to go green, the shortest path is sometimes it.skip or a looser assertion. Saying so in the block reason puts it right next to the failure.

Cursor: the same idea with followup_message

Cursor has its own hooks system (.cursor/hooks.json). Its stop hook can't block, but it can return a followup_message that Cursor submits automatically as the next user message, which works out to the same thing:

{
  "version": 1,
  "hooks": {
    "stop": [{ "command": "node .cursor/hooks/require-green.mjs", "loop_limit": 3 }]
  }
}
Enter fullscreen mode Exit fullscreen mode
// .cursor/hooks/require-green.mjs
import { spawnSync } from "node:child_process";
import { readFileSync } from "node:fs";

const input = JSON.parse(readFileSync(0, "utf8"));
if (input.status !== "completed") { console.log("{}"); process.exit(0); }

const run = spawnSync("npm", ["test", "--silent"], { encoding: "utf8" });
if (run.status === 0) { console.log("{}"); process.exit(0); }

const tail = `${run.stdout}\n${run.stderr}`.trim().split("\n").slice(-40).join("\n");
console.log(JSON.stringify({
  followup_message: `Tests are failing, so the task isn't done. Fix the code, not the tests:\n\n${tail}`,
}));
Enter fullscreen mode Exit fullscreen mode

Project hooks run from the repo root, so use .cursor/hooks/... paths, not ./hooks/.... loop_limit (default 5) caps how many automatic follow-ups one script can trigger.

CI: the check that doesn't care which agent wrote the code

Hooks run on one machine for one tool. A teammate on Copilot, a Codex cloud task, or you at 1 AM with hooks turned off all skip them. CI doesn't.

.github/workflows/test.yml:

name: test
on:
  pull_request:
  push:
    branches: [main]
jobs:
  test:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v7
      - uses: actions/setup-node@v7
        with:
          node-version: 24
          cache: npm
      - run: npm ci
      - run: npm test
Enter fullscreen mode Exit fullscreen mode

Then make the check required on main (Settings → Branches or Rulesets → require status checks to pass). A workflow nobody is required to pass is just a suggestion with a green badge.

If you run parallel agents, this is also where the integration problem gets caught. Each agent's branch can pass on its own. Merge them into one integration branch (or use a merge queue) and let CI run the full suite on the combined result before anything lands on main.

4. What stays in the rules file

Once tests are enforced, the rules file gets shorter and better. Use it to tell the agent what the checks are, so it runs them itself and the hook rarely has to fire:

## Definition of done
- `npm test` passes and `npm run typecheck` passes. A Stop hook and CI enforce this.
- Never skip, delete or weaken a test to make it pass. If a test is wrong, say so and stop.
- In your summary, list the commands you ran and their results. Don't claim checks you didn't run.
Enter fullscreen mode Exit fullscreen mode

That last line is underrated. It turns "all done! ✅" into something like "ran npm test: 142 passed", which is a claim you can actually check.

A quick sort for your existing rules:

Rule Where it goes
Naming, structure, preferred libraries Rules file (prose)
"Run X before saying done" Rules file and Stop hook
Tests/typecheck must pass to merge CI, required check
Never commit secrets, never edit generated files PreToolUse hook or scanner and CI
Your personal workflow habits Local file, gitignored

Wrap-up

  • Put rules where your tools actually read them, and keep one source of truth (AGENTS.md, imported from CLAUDE.md).
  • Commit what a reviewer would enforce; keep the rest local. Watch out for the CLAUDE.local.md gotcha.
  • Anything that must never break belongs in a Stop hook and a required CI check, not a paragraph.

If you want a starting point, I keep free, MIT-licensed rule sets for Next.js + Supabase + Stripe, Unity 6, Godot 4 and discord.js v14, in .mdc, AGENTS.md and CLAUDE.md formats:

👉 https://github.com/otctrading101/shiprules-free

If you'd rather not wire the hooks up yourself, the paid packs add the full rule sets plus ready-made Claude Code hooks, a zero-dependency scanner and a CI workflow at shiprules.gumroad.com. Code LAUNCH30 takes 30% off through Oct 22.

What's the most creative way you've seen an agent get to "done"? Tell me in the comments.


Disclosure: I make ShipRules (the paid packs above); the free GitHub repo is MIT. This post was written with AI assistance, and the hook and config details were checked against the Claude Code, Cursor and Codex docs in October 2026.

Top comments (0)