sangeeths.ai

Codex Harness Kit

One rule your coding agent can't ignore: you're not done until the tests pass. This is the exact setup from my Reel: Codex in Cursor, a Stop hook, and acceptance tests that belong to the harness, not to the agent.

Inspired by Jediah Katz's talk "Is the Agent Harness Dead?" (AI Engineer): models keep learning the clever tricks, but trust, infrastructure and efficiency stay the harness's job.

What it looks like

Codex wrote a first version and wanted to stop. The hook ran the tests and sent it back:

Blocked by hook: 2 of 6 acceptance tests fail
Blocked by hook: 2 of 6 acceptance tests fail

Codex fixed the code by itself. Only then the hook let it finish:

Harness check passed: 6/6 acceptance tests green
Harness check passed: 6/6 acceptance tests green

1. My Stop hook + hooks.json

Put this in your repo as .codex/hooks.json:

JSON
{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          { "type": "command", "command": "node \"$(git rev-parse --show-toplevel)/.codex/hooks/stop_require_tests.mjs\"", "timeout": 120 }
        ]
      }
    ]
  }
}

And this as .codex/hooks/stop_require_tests.mjs. When tests fail, it returns decision: "block", and Codex continues with the reason as its next prompt. After 3 failed tries it stops and hands over to you, so it can't loop forever.

JavaScript
#!/usr/bin/env node
// Stop hook: the agent is not done until the harness acceptance tests pass.
import { execFileSync } from "node:child_process";
import { readFileSync, existsSync, writeFileSync } from "node:fs";

const input = JSON.parse(readFileSync(0, "utf8") || "{}");
const root = input.cwd || process.cwd();
const counter = `${root}/.codex/.stop-attempts`;
const attempts = (existsSync(counter) ? Number(readFileSync(counter, "utf8")) : 0) + 1;

let report;
try {
  execFileSync("npx", ["vitest", "run", ".codex/acceptance", "--reporter=json", "--outputFile=.codex/.report.json"], { cwd: root, stdio: "ignore" });
} catch { /* failing tests exit non-zero; the report still gets written */ }
try { report = JSON.parse(readFileSync(`${root}/.codex/.report.json`, "utf8")); } catch { report = null; }

const total = report?.numTotalTests ?? 0;
const failed = report ? report.testResults.flatMap((f) => f.assertionResults.filter((a) => a.status !== "passed").map((a) => a.title)) : [];
const broken = !report || total === 0 || report.testResults.some((f) => f.status === "failed" && f.assertionResults.length === 0);

if (!broken && failed.length === 0) {
  writeFileSync(counter, "0");
  console.log(JSON.stringify({ systemMessage: `Harness check passed: ${total}/${total} acceptance tests green.` }));
} else if (attempts > 3) {
  writeFileSync(counter, "0");
  console.log(JSON.stringify({ systemMessage: `Harness check still failing after 3 tries (${failed.length} failing). Stopping for a human.` }));
} else {
  writeFileSync(counter, String(attempts));
  const list = broken ? "- the acceptance suite could not run" : failed.map((t) => `- ${t}`).join("\n");
  const head = broken ? "Harness check failed." : `Harness check failed: ${failed.length} of ${total} acceptance tests fail.`;
  console.log(JSON.stringify({ decision: "block", reason: `${head}\n${list}\nYou're not done yet. Fix the code in src/ (don't edit .codex/), then finish again.` }));
}

Add .codex/.report.json and .codex/.stop-attempts to your .gitignore.

2. Acceptance-test template

The trick: the tests live in .codex/acceptance/, owned by the harness. They encode your decisions (here: who counts as Pro), so the agent can't quietly pick the easy answer. Uses Vitest (npm i -D vitest).

TypeScript
// .codex/acceptance/billing.test.ts
import { describe, expect, it } from "vitest";
import { isPro } from "../../src/lib/billing";

const now = new Date("2026-10-21T12:00:00Z");
const inDays = (d: number) => new Date(now.getTime() + d * 86_400_000);
const user = (o: Record<string, unknown>) => ({ plan: "free", subscriptionStatus: null, currentPeriodEnd: null, ...o }) as any;

describe("who counts as Pro", () => {
  it("free user without a subscription is not Pro", () => {
    expect(isPro(user({}), now)).toBe(false);
  });
  it("14-day trial is Pro", () => {
    expect(isPro(user({ subscriptionStatus: "trialing", currentPeriodEnd: inDays(9) }), now)).toBe(true);
  });
  it("past_due keeps Pro while Stripe retries the card", () => {
    expect(isPro(user({ subscriptionStatus: "past_due", currentPeriodEnd: inDays(-2) }), now)).toBe(true);
  });
  it("cancelled keeps Pro until the paid period ends", () => {
    expect(isPro(user({ subscriptionStatus: "canceled", currentPeriodEnd: inDays(5) }), now)).toBe(true);
  });
});

Replace these with the rules of your own feature. One test per decision you'd hate the agent to get wrong.

3. How to trust hooks in Codex

  1. Start Codex in the repo: codex
  2. Codex shows "Hooks need review: 1 hook is new or changed". Hooks can run outside the sandbox, so Codex asks you first.
  3. Pick Review hooks, read the command, then trust it.
  4. Codex remembers the hook by its hash. If you change the file, it asks again.

Tip: Codex's own docs list this exact use case: "run a custom validation check when a chat turn stops". See developers.openai.com/codex/hooks.

4. The same hook for Claude Code

Claude Code speaks the same language for Stop hooks (decision: "block" + reason). Add this to .claude/settings.json and reuse the script:

JSON
{
  "hooks": {
    "Stop": [
      {
        "hooks": [
          { "type": "command", "command": "node \"$CLAUDE_PROJECT_DIR/.codex/hooks/stop_require_tests.mjs\"" }
        ]
      }
    ]
  }
}

Why this matters


Made by @sangeeths.ai: real AI engineering, recorded live. Follow on Instagram for more. Share this kit: sangeeths.ai/harness · All free kits: sangeeths.ai

The weekly kit in your inbox

One email a week with the newest kit and the AI news worth your time.

More free kits

See all kits