Build an AI Coding Loop with Claude Code

Build an autonomous maker-checker loop using Claude Code hooks and subagents.

Introduction

30 Second Summary

Every time you use an AI coding assistant, you type a prompt, wait for the response, check the output, and type another prompt. You are the loop.

In this project, you will build a loop engineering setup around a small Node.js project using Claude Code. You will configure a memory file, create subagents, wire up hooks, and run a goal-based loop that autonomously codes, tests, reviews, and commits.

What You'll Build

You type a single command in your terminal, walk away, and come back to find a new feature implemented, tests passing, code formatted, and clean commits sitting in your Git history.

By the end of this project, you'll have:

  • A CLAUDE.md spine that makes every Claude Code session start with your exact project conventions. No more generic guesses or cold starts.
  • Hooks that auto-format every file Claude edits and block destructive commands before they execute. These fire whether the model remembers them or not.
  • A maker-checker loop where one agent writes code and a separate agent reviews it. You trigger the full cycle with a single slash command, and it keeps iterating until tests pass and the reviewer approves.
  • Secret Mission: Wire your loop into a GitHub Actions workflow so it triggers automatically on every pull request.

Do I need a Claude subscription?

Claude Code requires an active Claude subscription (Pro, Max, or Team plan). API usage within Claude Code sessions is included in your subscription, so there is no additional cost for the main project.

Before We Start

Before you install anything or write a single line of configuration, let's lock in what you're about to build and why it matters to you. Think about a real project where you prompt an AI coding agent turn by turn, and what would change if you could hand that off to a system that does it automatically.

Install Claude Code and Initialize Your Project

Every loop needs something to loop on. Before you can configure hooks, subagents, or slash commands, you need a real project with files to edit and tests to run.

In this step, you will install the Claude Code CLI, create a small Node.js project with a passing test suite, and lock it all into a clean Git repository.

In this step, get ready to:
  • Install and authenticate Claude Code.
  • Create a Node.js project with a utility function and passing tests.
  • Initialize a Git repository with a clean initial commit.
Install and authenticate Claude Code

Claude Code is a CLI tool that reads your codebase, edits files, and runs commands from your terminal. It requires a Claude subscription (Pro, Max, or Team plan).

  • Press Cmd+Space to open your search bar.
  • Type Terminal and press Enter to open it.
  • Install Claude Code by running this command:
curl -fsSL https://claude.ai/install.sh | bash

What does this command do?

This downloads and runs Anthropic's official install script. It installs the claude binary on your machine so you can use it from any terminal session.

Seeing a curl error or syntax error?

If the command fails with a syntax error or a 403 response, visit Anthropic's installation troubleshooting page for alternative install methods.

Help me troubleshoot the Claude Code installation error I'm seeing.

  • Verify the installation by running:
claude --version

You should see a version number printed in your terminal.

Now it is time to authenticate. When you run claude for the first time, it opens your browser so you can sign in to your Claude account.

  • Start a Claude Code session by running:
claude
  • Your browser opens to a login page. Sign in with your Claude Pro, Max, or Team account.
  • After signing in, return to your terminal. You should see the Claude Code interactive prompt ready for input.
  • Type /exit to close the session for now. You will open it again inside your project directory later.

Browser didn't open?

If the browser did not open automatically, look for a URL printed in your terminal. Copy it and paste it into your browser manually to complete the login.

Help me authenticate Claude Code when the browser doesn't open.

Create a Node.js project with a utility function and tests

Your loop needs a project to act on. You will create a small Node.js project with one utility function and one test. This gives the loop real code to edit and a real test suite to run.

The project uses Node's built-in test runner (node:test), which requires Node.js 18 or higher. Check which version you have by running:

  • Check your Node.js version by running:
node --version

✔️ I see version 18 or higher

Your Node.js version supports the built-in test runner. You are ready to create the project.

ⓧ I see a version lower than 18

You need Node.js 18 or higher for the node:test runner. Download the latest LTS installer from the official Node.js download page and run it. The installer replaces your current version.

After installing, close and reopen your terminal, then run node --version again to confirm the new version.

ⓧ Command not found

Node.js is not installed on your machine. Download the latest LTS installer from the official Node.js download page and run it.

After installing, close and reopen your terminal, then run node --version again to confirm it prints a version number.

  • Move to your Desktop by running this command:
cd ~/Desktop
  • Create your project folder and move into it by running these commands:
mkdir loop-project
cd loop-project
  • Create the src and test directories by running:
mkdir src test
  • Confirm the folders exist by running:
ls

You should see src and test listed in the output.

Now you will create three files: a package.json that defines the project, a source file with a utility function, and a test file that verifies it.

  • Create package.json by running this command:
cat > package.json << 'EOF'
{"name": "loop-project", "version": "1.0.0", "type": "module", "scripts": {"test": "node --test"}}
EOF

What does this package.json do?

  • "type": "module" enables ES module syntax (import/export) instead of CommonJS require().
  • "test": "node --test" wires up Node's built-in test runner so npm test finds and runs every test file automatically.
  • Create the utility function in src/utils.js by running:
cat > src/utils.js << 'EOF'
export function add(a, b) {
  return a + b;
}
EOF
  • Create the test file in test/utils.test.js by running:
cat > test/utils.test.js << 'EOF'
import { test } from 'node:test';
import assert from 'node:assert';
import { add } from '../src/utils.js';

test('add returns the sum of two numbers', () => {
  assert.strictEqual(add(2, 3), 5);
});
EOF

What does this test do?

The test imports the add function from src/utils.js and checks that add(2, 3) returns 5. If it does not, the test fails and the runner reports the mismatch.

Before you run the tests, what do you think will happen? You created one test that checks a simple addition. Do you expect it to pass or fail?

  • Run the test suite by running:
npm test

You should see output showing one test passed. Look for a line containing pass 1 and fail 0.

Tests failing or not found?

  • Make sure your package.json includes "type": "module". Without it, the import statements in the test file will fail.
  • Check that your test file is inside the test/ directory and named utils.test.js. The Node test runner looks for files matching the *.test.js pattern.
  • Verify your Node.js version is 18 or higher with node --version. The built-in test runner is not available in older versions.

Help me figure out why my Node.js tests are failing.

✔️ Awesome, I've got everything!

Great. Confirm you have saved all three files by running:

ls && ls src && ls test

You should see package.json, src, and test in the first line, utils.js in the second, and utils.test.js in the third.

ⓧ I'd like to double check the full code

Here are all three files. Compare each one to what you have in your project.

package.json:

{"name": "loop-project", "version": "1.0.0", "type": "module", "scripts": {"test": "node --test"}}

src/utils.js:

export function add(a, b) {
  return a + b;
}

test/utils.test.js:

import { test } from 'node:test';
import assert from 'node:assert';
import { add } from '../src/utils.js';

test('add returns the sum of two numbers', () => {
  assert.strictEqual(add(2, 3), 5);
});
Initialize a Git repository

Claude Code tracks changes with Git. Initializing a repository now gives the loop a clean baseline to diff against when it starts making changes.

  • Check that Git is available by running:
git --version

✔️ I see a version number

Git is installed and ready to use.

ⓧ A dialog asks me to install developer tools

macOS ships a shim that triggers the Xcode Command Line Tools installer the first time you use Git. Click Install in the dialog and wait for the installation to finish. This may take a few minutes.

After it completes, run git --version again to confirm it prints a version number.

  • Initialize a new Git repository by running:
git init
  • Stage all files and create your initial commit by running:
git add .
git commit -m "Initial commit"

Seeing a message about configuring your identity?

If Git asks you to set your name and email, run these two commands first, replacing the placeholder values with your own name and email:

git config --global user.name "your-name-here"
git config --global user.email "your-email-here"

Then run the git commit command again.

What do you expect git status to show after a successful commit with no new changes?

  • Verify your repository is clean by running:
git status

You should see a message that says nothing to commit, working tree clean. This confirms all your files are committed.

Your project has code, passing tests, and a clean Git history. Next up, you will create the CLAUDE.md file that tells Claude Code exactly how to work in this project.

Write Your CLAUDE.md and See It Change Claude's Behavior

Your project has a test suite and a clean Git history. Claude Code can open a session and start working with it right now.

The problem is that Claude Code starts every session cold. Without guidance, it fills in the gaps with confident guesses about your conventions, your file structure, and your definition of "done." A CLAUDE.md file fixes that. It is a markdown file at your project root that Claude Code reads at the start of every session, acting as persistent project context. Every other loop component you build later (hooks, subagents, slash commands) will read from this file as the single source of truth.

In this step, get ready to:
  • Create a CLAUDE.md file with orientation, architecture, and review checklist sections.
  • See the concrete difference between a Claude Code session with and without your CLAUDE.md.
Create your CLAUDE.md file

CLAUDE.md is the spine of your loop. It tells Claude Code what the project is, how it is structured, and what "done" looks like. Every session reads it automatically before the first prompt.

  • Create a new file called CLAUDE.md at the root of your loop-project folder and paste the following content into it:
# Project: loop-project

## Orientation
This is a Node.js (ESM) utility library used as a loop engineering test bed. Runtime: Node 20+. Language: JavaScript (ESM, no TypeScript).

## Architecture
- Source files live in `src/`. One function per file, named after the function.
- Tests live in `test/`. Each test file mirrors its source file (e.g., `src/utils.js` → `test/utils.test.js`).
- Use Node's built-in test runner (`node --test`). No external test frameworks.
- All identifiers use camelCase. No default exports.

## Review Checklist
Before considering work done, verify:
- [ ] All tests pass (`npm test`).
- [ ] No files have formatting errors (Prettier clean).
- [ ] New functions have at least one edge-case test.
- [ ] No destructive shell commands were run.
- [ ] Commit message is present and descriptive.

What do the three sections do?

  • Orientation tells Claude what the project is, what runtime it uses, and what language to write in. This prevents Claude from guessing or defaulting to TypeScript.
  • Architecture defines the file layout and naming conventions. Claude now knows where source and test files go, and that node --test is the only test runner.
  • Review Checklist is the definition of "done." Later, your reviewer subagent will check every item on this list before approving work.
  • Save CLAUDE.md.
  • Confirm the file exists by running:
ls CLAUDE.md

You should see CLAUDE.md printed in the output.

Don't see CLAUDE.md?

  • Make sure you are in the loop-project directory. Run pwd to check.
  • Confirm the filename is exactly CLAUDE.md (uppercase). Claude Code looks for this specific filename.
  • Help me troubleshoot my CLAUDE.md file.

✔️ Awesome, I've got everything!

Great. Double check that you have saved CLAUDE.md before continuing.

ⓧ I'd like to double check the full code

Your CLAUDE.md should contain exactly this:

# Project: loop-project

## Orientation
This is a Node.js (ESM) utility library used as a loop engineering test bed. Runtime: Node 20+. Language: JavaScript (ESM, no TypeScript).

## Architecture
- Source files live in `src/`. One function per file, named after the function.
- Tests live in `test/`. Each test file mirrors its source file (e.g., `src/utils.js` → `test/utils.test.js`).
- Use Node's built-in test runner (`node --test`). No external test frameworks.
- All identifiers use camelCase. No default exports.

## Review Checklist
Before considering work done, verify:
- [ ] All tests pass (`npm test`).
- [ ] No files have formatting errors (Prettier clean).
- [ ] New functions have at least one edge-case test.
- [ ] No destructive shell commands were run.
- [ ] Commit message is present and descriptive.
See the difference CLAUDE.md makes

The best way to understand what CLAUDE.md does is to see Claude Code with it and without it, back to back.

  • Start a Claude Code session inside your loop-project directory by running:
claude

What happens when Claude Code starts?

Claude Code reads your CLAUDE.md file automatically at the start of the session. You do not need to paste or reference it. Everything you wrote in that file is now part of Claude's context.

  • Once the interactive session starts, type the following prompt and press Enter:
What are the project conventions and what must be true before work is considered done?

Claude's response should reference the exact conventions from your CLAUDE.md: ESM modules, camelCase identifiers, node --test as the test runner, and the review checklist items.

  • Exit the session by typing /exit and pressing Enter.

Now you will start a second session that deliberately skips CLAUDE.md. Before you run it, think about what you expect Claude to say about the project conventions this time. Will it know about your review checklist? Your naming conventions?

  • Start a bare session that skips all project configuration by running:
claude --bare

What does --bare do?

The --bare flag starts Claude Code in minimal mode. It skips auto-discovery of hooks, skills, custom commands, subagents, plugins, MCP servers, auto memory, and CLAUDE.md. This gives you a clean baseline to compare against.

  • Type the same prompt and press Enter:
What are the project conventions and what must be true before work is considered done?

This time Claude gives generic answers. It might guess at conventions or say it does not have that information. It has no idea about your review checklist, your file structure rules, or your testing approach.

  • Exit the session by typing /exit and pressing Enter.

Why does this matter for the loop?

Every component you build from here on (hooks, subagents, slash commands) depends on Claude knowing your project conventions. Without the CLAUDE.md spine, each of those components would need to restate the rules themselves. With it, they all read from one source of truth.

Your CLAUDE.md is locked in and every Claude Code session now starts with your project's conventions baked in. Next up, you will add hooks that enforce those conventions automatically, even when the model tries to ignore them.

Add Hooks That Enforce Rules Claude Cannot Ignore

Your CLAUDE.md now loads at the start of every Claude Code session, giving the model a clear set of conventions to follow. But the model can still drift from those instructions mid-session.

Hooks solve this problem. A hook is a deterministic shell command that fires at a specific lifecycle point, whether the model remembers the rules or not. In this step, you will wire up two hooks: one that auto-formats every file Claude edits, and one that blocks dangerous commands before they run.

In this step, get ready to:
  • Create a PostToolUse hook that auto-formats every file Claude edits.
  • Add a PreToolUse hook that blocks destructive commands before they execute.
Set up the auto-format hook

Claude Code hooks fire at specific points in the tool lifecycle. A PostToolUse hook runs after Claude uses a tool. By matching on Edit|Write, you can catch every file modification and pipe it through a formatter automatically.

The hook you are about to create uses jq to extract the edited file path from the hook's JSON input, then passes it to Prettier. First, confirm jq is available on your machine.

  • Check that jq is installed by running:
jq --version

✔️ I see a version number

You are all set. Move on to the next instruction.

ⓧ Command not found

You need to install jq before continuing. The fastest way on macOS is with Homebrew.

  • Install jq by running:
brew install jq

Don't have Homebrew either?

Download the prebuilt jq binary for your Mac from the official jq download page. Move the downloaded binary into /usr/local/bin/ and make it executable with chmod +x jq.

  • After installing, confirm it works by running jq --version again.

Next, cache Prettier so the hook can call it without an interactive prompt.

  • Cache Prettier by running:
npx -y prettier --version

You should see a version number printed, confirming Prettier is cached and ready.

  • Create the .claude configuration directory by running:
mkdir -p .claude
  • Confirm it exists by running:
ls -a

You should see .claude listed alongside your existing files and folders.

Now create the settings file with your first hook. Claude Code reads hook configuration from .claude/settings.json.

  • Create a new file called settings.json inside the .claude folder. Paste the following code into it:
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
          }
        ]
      }
    ]
  }
}

What does this hook do?

This is a PostToolUse hook, which means it runs after Claude finishes using a tool.

  • The matcher field "Edit|Write" tells Claude Code to fire this hook only when Claude edits or writes a file. Other tool calls are ignored.
  • The command receives the hook event as JSON on stdin. jq extracts the file path, and xargs passes it to Prettier for formatting.
  • The result: every file Claude touches gets auto-formatted instantly, with no reliance on the model remembering to format.
  • Save .claude/settings.json.
  • Verify the file was created correctly by running:
cat .claude/settings.json

You should see your PostToolUse hook configuration printed in the terminal.

Seeing an empty file or an error?

Make sure you saved the file inside the .claude directory, not at the project root. The path should be .claude/settings.json, not settings.json.

Also check that the JSON is valid. A missing comma or bracket will cause Claude Code to ignore the file silently.

Help me fix my .claude/settings.json file

Add the PreToolUse blocking hook

A PostToolUse hook runs after the damage is done. For destructive commands like rm -rf or git push --force, you need to intercept the command before it executes.

A PreToolUse hook fires before Claude runs a tool. If the hook exits with code 2, Claude Code blocks the tool call entirely and shows the error message to Claude. This is a hard gate, not a suggestion.

  • Open .claude/settings.json and replace its entire contents with the following, which adds the PreToolUse hook alongside your existing PostToolUse hook:
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
          }
        ]
      }
    ],
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "python3 -c \"import sys, json; cmd = json.load(sys.stdin).get('tool_input', {}).get('command', ''); bad = ['rm -rf', 'git push --force', 'git push origin main']; [sys.exit(2) for p in bad if p in cmd]\""
          }
        ]
      }
    ]
  }
}

What does the PreToolUse hook do?

This hook matches on the Bash tool, which means it fires before any shell command Claude wants to run.

  • The command reads the proposed shell command from stdin using python3 and checks it against a blocklist: rm -rf, git push --force, and git push origin main.
  • If the command matches, the script exits with code 2. Exit code 2 is Claude Code's signal for a blocking error. Claude sees the error in stderr and the tool call is prevented.
  • Exit code 0 means the command is safe and Claude can proceed. Any other non-zero exit code is a non-blocking error that Claude sees but can still continue past.
  • Save .claude/settings.json.
  • Verify both hooks are present by running:
cat .claude/settings.json

You should see both PostToolUse and PreToolUse sections in the output.

Only seeing one hook?

The most common mistake is a missing comma between the PostToolUse array and the PreToolUse key. Look for the closing ], after the PostToolUse block. Without that comma, the JSON is invalid and Claude Code silently ignores the file.

Help me fix the JSON in my settings.json

Test both hooks in a live session

Both hooks are configured. Time to see them work. You will start a Claude Code session, trigger the auto-format hook by editing a file, then trigger the blocking hook by requesting a destructive command.

  • Start a Claude Code session by running:
claude
  • Once the session starts, type this prompt to trigger the PostToolUse hook:
Add a comment to the top of src/utils.js that says "// Utility functions for loop-project"

After Claude edits the file, watch the terminal output. You should see Prettier run automatically on src/utils.js right after the edit completes. The hook fires without Claude deciding to format. It just happens.

What should I see?

Look for output indicating that Prettier processed src/utils.js after the edit. The exact format depends on your Prettier version, but you will see the filename appear in the hook's output, confirming the auto-format ran.

Now test the blocking hook. Before you type the next prompt, ask yourself: what do you think will happen when you ask Claude to run a destructive command?

  • In the same Claude Code session, type this prompt:
Run the command rm -rf /

The PreToolUse hook intercepts the command before it executes. Claude sees a blocking error and the command never runs. This is the difference between a rule the model reads and a rule the system enforces.

  • Exit the Claude Code session by pressing Ctrl+C or typing /exit.

Hook didn't fire?

If Prettier did not run after the edit, or the destructive command was not blocked, check that .claude/settings.json is valid JSON. Even a single missing quote or bracket causes Claude Code to skip the hooks silently.

Run python3 -m json.tool .claude/settings.json to validate the file. If it prints the formatted JSON, the syntax is correct. If it prints an error, fix the location it points to.

Help me debug why my Claude Code hooks aren't firing

✔️ Awesome, I've got everything!

Great. Double check you have saved .claude/settings.json.

ⓧ I'd like to double check the full code

Here is the complete .claude/settings.json with both hooks. Make sure your file matches this exactly:

{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "jq -r '.tool_input.file_path' | xargs npx prettier --write"
          }
        ]
      }
    ],
    "PreToolUse": [
      {
        "matcher": "Bash",
        "hooks": [
          {
            "type": "command",
            "command": "python3 -c \"import sys, json; cmd = json.load(sys.stdin).get('tool_input', {}).get('command', ''); bad = ['rm -rf', 'git push --force', 'git push origin main']; [sys.exit(2) for p in bad if p in cmd]\""
          }
        ]
      }
    ]
  }
}

Your hooks are live. Every file edit gets auto-formatted, and every dangerous command gets blocked before it touches your system. Next up, you will split Claude into two agents: one that writes code and one that reviews it.

Create Subagents That Split the Maker from the Checker

Your hooks now enforce rules at the system level. No matter what Claude tries, Prettier formats every file and destructive commands get blocked. But there is still a gap in your loop.

When Claude writes code and then checks its own work, the same model is grading itself. It is too generous with its own output. The fix is the maker-checker pattern. You create two subagents with different instructions and different tool access. One builds. The other inspects. Since the reviewer never wrote the code, it has no bias toward approving it.

In this step, get ready to:
  • Create a coder subagent that implements features, runs tests, and commits passing work.
  • Create a reviewer subagent with read-only tools that verifies work against the CLAUDE.md checklist.
  • Test the reviewer subagent by invoking it on the current project.
Create the coder subagent

Subagents are markdown files with YAML frontmatter that live in the .claude/agents/ directory. Claude Code discovers them automatically and delegates tasks based on each subagent's description.

  • Create the agents directory and the coder subagent file by running these commands:
mkdir -p .claude/agents
touch .claude/agents/coder.md
  • Confirm the file was created by running:
ls .claude/agents/

You should see coder.md in the output.

  • Open coder.md in your editor and paste the following content:
---
name: coder
description: Implements features described in task.md, runs tests after each change, and commits passing work.
tools: Read, Write, Edit, Bash
---

You are the coding agent in a maker-checker loop. Your responsibilities:
1. Read the current task from `task.md`.
2. Implement the described feature following all conventions in `CLAUDE.md`.
3. Run `npm test` after every meaningful change.
4. If tests fail, diagnose and fix before proceeding.
5. Once all tests pass, stage and commit your changes with a descriptive commit message.
6. Do not consider the task done until tests pass and your commit is clean.

What does this file do?

The YAML frontmatter between the --- delimiters configures the subagent. name is a unique identifier. description tells Claude Code when to delegate tasks to this agent.

The tools field is an allowlist. This coder gets Read, Write, Edit, and Bash because it needs full access to implement features, run tests, and commit. A subagent cannot call tools outside its configured list, even if its system prompt tells it to.

Everything below the frontmatter is the subagent's system prompt. It replaces Claude Code's default system prompt for this agent, giving it a focused set of instructions.

  • Save coder.md.
Add the reviewer subagent and test it

The reviewer is the other half of the maker-checker split. It gets a deliberately restricted tool set so it can inspect code without modifying it.

  • Create the reviewer subagent file by running:
touch .claude/agents/reviewer.md
  • Open reviewer.md in your editor and paste the following content:
---
name: reviewer
description: Reviews the coder's changes against the CLAUDE.md checklist and produces a structured PASS or NEEDS CHANGES verdict.
tools: Read, Grep, Bash
---

You are the reviewer agent in a maker-checker loop. You have read-only access. Your responsibilities:
1. Run `git diff HEAD~1` to inspect the coder's latest changes.
2. Run `npm test` to confirm tests pass.
3. Check each item in the CLAUDE.md review checklist.
4. Produce a structured review with sections: Correctness, Test Coverage, Style.
5. End with a clear verdict:
   - **PASS (LGTM)** if all checklist items are satisfied.
   - **NEEDS CHANGES** with a numbered list of specific findings if anything fails.
Do not suggest vague improvements. Every finding must reference a specific file and line.

Why read-only tools for the reviewer?

The reviewer gets Read, Grep, and Bash (scoped to inspection commands like git diff and npm test). It does not get Write or Edit. This is deliberate. A reviewer that can edit files is tempted to fix what it finds instead of reporting it. Keeping it read-only forces clear, specific verdicts that the coder must act on.

  • Save reviewer.md.
  • Confirm both subagents exist by running:
ls .claude/agents/

You should see both coder.md and reviewer.md listed.

✔️ Awesome, I've got everything!

Great. Double check that you have saved both files.

ⓧ I'd like to double check the full code

Scroll up and confirm your two files match the code blocks above exactly. coder.md should have four frontmatter fields (name, description, tools) and a six-step system prompt. reviewer.md should have three frontmatter fields and a five-step system prompt ending with the PASS/NEEDS CHANGES verdict format.

Now let's see the reviewer in action. You will invoke it inside a Claude Code session and watch it produce a structured review of your project.

  • Switch back to your Claude Code interactive session. If you exited it, start a new one by running:
claude

Before you run this next prompt, think about what you expect the reviewer to report. Your project has one function, one test, and a clean commit. Do you think it will pass or flag something?

  • In the Claude Code session, type this prompt and press Enter:
Use the reviewer agent to review the current project

What should I see?

Claude Code delegates to the reviewer subagent. In the transcript you will see a tool call row showing the subagent's name followed by a short task description, such as reviewer (Review current project).

The reviewer runs git diff HEAD~1 and npm test, checks each item in the CLAUDE.md review checklist, and produces a structured review with Correctness, Test Coverage, and Style sections. It ends with a clear verdict: either PASS (LGTM) or NEEDS CHANGES with specific findings.

Reviewer not producing a structured verdict?

If the reviewer gives a generic "looks good" instead of the structured Correctness/Test Coverage/Style format, Claude may not have found the subagent file. Exit Claude Code, run ls .claude/agents/reviewer.md to confirm the file exists, then restart Claude Code with claude and try the prompt again. Claude Code discovers new agent files on session start.

If the reviewer still does not produce a structured review, check that your reviewer.md frontmatter has name: reviewer and that the YAML is between --- delimiters.

Still stuck? Help me troubleshoot why my reviewer subagent is not producing a structured PASS or NEEDS CHANGES verdict.

You now have two subagents with distinct roles: one that builds and one that inspects. The reviewer cannot edit what it reviews, and the coder cannot skip the review. Next up, you will wire these pieces into a single goal-based loop that runs autonomously until the work is done.

Run Your First Goal-Based Loop

In the last step, you split your loop into a maker and a checker. The coder subagent writes code and the reviewer subagent verifies it. But right now, each piece still lives in its own file. You invoke them separately. Nothing ties them into a loop that runs on its own.

The /goal command is what turns those separate pieces into an autonomous system. You give it a finish line in plain language, and Claude Code keeps working across turns until a separate evaluator model confirms that finish line is met. This is the moment loop engineering stops being configuration and becomes a working loop.

In this step, get ready to:
  • Write a task file that gives the loop a concrete feature to implement.
  • Create a slash command that triggers code review with a single keystroke.
  • Run the full goal-based loop and watch Claude Code autonomously code, test, review, and commit.
Write a task file for the loop

Every goal-based loop needs a clear target. Instead of typing the task into the prompt, you write it in a file that the coder subagent reads. This keeps the task versioned, reusable, and separate from the loop itself.

  • Create a task.md file at the root of your loop-project folder by running:
touch task.md
  • Confirm the file exists by running:
ls

You should see task.md listed alongside your other project files.

  • Open task.md in your editor and paste the following task definition:
# Task: Add clamp utility

Add a `clamp(value, min, max)` function to `src/utils.js`.

## Requirements
- Returns `value` if it is between `min` and `max` (inclusive).
- Returns `min` if `value` is below the range.
- Returns `max` if `value` is above the range.
- Throws a `RangeError` if `min > max`.

## Success Criterion
All tests pass (`npm test`) and the reviewer subagent gives LGTM.

What does this task file do?

The task file is the input to your loop. The coder subagent's system prompt (in coder.md) tells it to read task.md for its assignment. By defining requirements and a success criterion in the file, you give the loop a measurable target it can verify.

The success criterion is the key piece. It tells the loop exactly what "done" looks like: tests pass and the reviewer approves.

  • Save task.md.
Create the /review-pr slash command

A slash command is a reusable prompt saved as a markdown file. The filename becomes the command name, so review-pr.md creates /review-pr. You will use this to trigger the reviewer subagent on staged changes with a single keystroke.

  • Create the .claude/commands/ directory and the slash command file by running:
mkdir -p .claude/commands
touch .claude/commands/review-pr.md
  • Confirm the file was created by running:
ls .claude/commands/

You should see review-pr.md listed.

  • Open .claude/commands/review-pr.md in your editor and paste the following:
---
name: review-pr
description: Triggers the reviewer subagent on staged changes and produces a structured PASS or NEEDS CHANGES verdict.
---

Run the reviewer subagent on the current staged changes. The reviewer should:
1. Inspect staged changes with `git diff --cached`.
2. Run `npm test`.
3. Check each item in the CLAUDE.md review checklist.
4. Output a structured review (Correctness, Test Coverage, Style) and a clear PASS (LGTM) or NEEDS CHANGES verdict.

What does this slash command do?

The YAML frontmatter at the top (between the --- lines) sets a name and description that show up in Claude Code's command picker. The body below the frontmatter is the prompt itself.

When you type /review-pr in a Claude Code session, Claude reads this file and executes the instructions. It triggers your reviewer subagent with its read-only tool set, so the review is independent from the coder's work.

  • Save .claude/commands/review-pr.md.
  • Switch back to your Claude Code session in the terminal (or start a new one with claude if you exited).
  • Type /review-pr and press Enter.

The reviewer subagent runs against the current state of your project. Since you have not made any new changes yet, it should report that everything looks clean and give a PASS verdict.

Command not showing up?

If /review-pr does not appear in the command list, exit your Claude Code session and start a fresh one with claude. Claude Code discovers new slash commands when a session starts.

Also check that the file is at .claude/commands/review-pr.md (not .claude/review-pr.md). The commands/ subdirectory is required.

Why isn't my /review-pr slash command showing up in Claude Code?

Run the full loop with /goal

You have every piece in place: the CLAUDE.md spine, hooks that enforce formatting and block dangerous commands, a coder subagent, a reviewer subagent, a task file with a clear target, and a slash command for review. Now you tie them all together with a single command.

What is /goal?

The /goal command sets a completion condition in plain language. After each turn Claude takes, a separate small evaluator model checks whether the condition is met. If not, Claude keeps working. If yes, the loop stops automatically.

This is different from regular prompting where Claude does one thing and waits for you. With /goal, you describe the finish line and walk away. A ◎ /goal active indicator shows progress while it runs.

Before you run the loop, take a moment to predict what will happen. The coder subagent will read task.md and implement clamp. The PostToolUse hook will auto-format every file it edits. Then the reviewer subagent will check the work. Do you think the reviewer will approve on the first try, or will it find something the coder missed?

  • In your Claude Code session, type the following and press Enter:
/goal Read task.md and implement the described feature using the coder subagent. After the coder commits, run the reviewer subagent to verify the work. Success: npm test exits 0 and the reviewer gives LGTM. Stop after 10 turns.

What makes this a strong /goal condition?

A good /goal condition has three parts: a measurable end state (npm test exits 0 and reviewer gives LGTM), a clear way for Claude to prove it (run the tests, run the reviewer), and a safety bound (stop after 10 turns).

The turn cap prevents the loop from running forever if something goes wrong. Without it, a condition that can never be met would keep Claude spinning indefinitely.

Watch the loop run. You will see a ◎ /goal active indicator with elapsed time and turn count. Claude works through the cycle: the coder reads the task, writes the clamp function and its tests, the PostToolUse hook auto-formats every file edit, the coder runs npm test, and the reviewer checks the work against the CLAUDE.md checklist.

If the reviewer finds an issue (a missing edge-case test, a formatting problem, a checklist violation), it sends a NEEDS CHANGES verdict. The loop does not stop. The coder gets another turn to fix the finding and the reviewer checks again. This correction cycle is the whole point of the maker-checker split. You are watching two agents hold each other accountable without your input.

Cancel mid-run if needed

If the loop seems stuck or you want to stop early, type /goal clear to cancel the active goal. Your progress is preserved in the conversation transcript.

Once the loop finishes, the ◎ /goal active indicator disappears and you regain control of the session.

  • Verify the loop's work by checking the Git log. Exit the Claude Code session and run:
git log --oneline -5

You should see one or more new commits made by the loop with descriptive commit messages about the clamp function.

  • Confirm the feature works by running:
npm test

All tests should pass, including the new tests for clamp.

  • Inspect the implementation by running:
cat src/utils.js

You should see the original add function plus a new clamp function that handles the three range cases and throws a RangeError when min > max.

Tests failing or clamp function missing?

If npm test fails, check whether the loop completed all its turns. Run git log --oneline -5 to see if any commits were made. If there are no new commits, the loop may have hit the turn cap before finishing.

Start a new Claude Code session with claude and run the /goal command again. The loop picks up from where your code currently stands.

My /goal loop finished but the tests are failing. Help me debug what went wrong.

You just ran an autonomous coding loop. You gave Claude Code a task, a finish line, and a set of rules. It implemented a feature, formatted the code, verified its own work with a second agent, and committed the result. All without you typing a single line of code. Next up, you will wire this same loop into a CI pipeline so it runs automatically on every pull request.

Secret mission

Wire Your Loop into GitHub Actions

Your loop runs when you are at your terminal. In this mission, you wire it into a GitHub Actions workflow so it triggers automatically on every pull request, reviewing code while you are away.

Clean Up Your Resources

Clean Up Your Resources

Decide whether to keep your resources running, pause them to come back later, or delete them entirely. This project runs locally, so there are no ongoing infrastructure costs beyond your Claude subscription.

Cost warning

If you completed the Secret Mission, your GitHub Actions workflow triggers Claude Code on every pull request event. Each run consumes Anthropic API tokens billed to the ANTHROPIC_API_KEY stored in your repository secrets. Disable or delete the workflow to stop these charges.

Resources you used:

  • Local loop-project directory with source files, tests, CLAUDE.md, hooks, subagents, and slash commands.
  • GitHub repository with test branches and pull requests.
  • ANTHROPIC_API_KEY stored as a GitHub repository secret (Secret Mission).
  • GitHub Actions workflow (claude-review.yml) that runs on pull request events (Secret Mission).

Keep everything running

No action needed. Choose this if you plan to keep building on your loop engineering setup.

  • Your loop configuration lives in .claude/ and CLAUDE.md. It activates automatically in every Claude Code session you start in this project directory.
  • If you completed the Secret Mission, your GitHub Actions workflow continues to trigger on every pull request. This consumes API tokens billed to your Anthropic account.

Pause - I'll come back to this later

Stop any ongoing API costs while keeping your files intact for later.

  • If you completed the Secret Mission, disable the GitHub Actions workflow. Open the Actions tab in your GitHub repository, find the Claude PR Review workflow, and disable it. This stops API token consumption on future pull requests.
  • Your local project files cost nothing to keep on disk. Leave them as they are.

Delete - I don't want to use this again

Remove all project resources and start fresh.

  • Delete the local project folder by running:
rm -rf ~/Desktop/loop-project
  • If you completed the Secret Mission, delete the ANTHROPIC_API_KEY from your GitHub repository. Go to your repository's Settings, then Secrets and variables, then Actions. Delete the ANTHROPIC_API_KEY secret.
  • Delete the GitHub repository. Open the repository's Settings page and follow the steps to delete the repository.

Nice Work!

Nice Work!

Nice work! You just built a complete loop engineering setup that turns Claude Code from a chatbot you prompt into an autonomous engineering system that finds work, does it, checks it, and stops when done.

You've learned how to:

  • Build a CLAUDE.md spine that gives every Claude Code session your project conventions, architecture decisions, and review checklist so the model never starts a session cold.
  • Wire PostToolUse and PreToolUse hooks that auto-format code and block destructive commands deterministically, firing whether the model wants them to or not.
  • Create a maker-checker subagent split and run a goal-based loop that autonomously iterates through code-test-review cycles until all tests pass and the reviewer gives LGTM.
  • Secret Mission: Wire the loop into GitHub Actions so it triggers automatically on pull requests, shifting from manual loop invocation to event-driven automation.

Ready to quiz yourself?