この記事の位置づけ
この記事は、@Syoitu 氏の Qiita 記事 入門から実践 -「🔁 ループエンジニアリング」 を土台にした、手を動かすための実践編です。
元記事は「ループエンジニアリングとは何か」を概念から広く解説し、その中の「Claude Code で最初のループを組む」節は、すでにある React / Next.js / TypeScript プロジェクトを前提にしていました。本記事はそこを切り出し、プロジェクトを持っていない人でも試せるよう、FizzBuzz の練習環境をゼロから組む形に作り直したものです。用語や考え方は元記事に準拠しています。
ループエンジニアリング(loop engineering): AI エージェント運用の設計手法を指す言葉。「エージェントにプロンプトを打つ人」を自分から外し、代わりに打ち続ける仕組み(ループ)を設計する、という考え方。2026 年 6 月ごろに定着した比較的新しい用語です。
この記事のゴール
Claude Code に「テストが通るまで直して」と一度頼むだけで、AI が自分でコードを書き、テストで失敗を見つけ、直し、また試す——この往復を人が介入せずに回す。それを小さく確実な例で腹落ちさせるのがゴールです。
難しい実プロジェクトは使いません。狙いは「ループが何によって駆動されているか」を、FizzBuzz という正解が一意に決まるお題で体験することです。
そもそもループエンジニアリングとは
ふつうの AI との対話は「指示 → 返答 → また指示」の直線です。人が毎回プロンプトを打ち、結果を確認し、次を指示します。
ループエンジニアリングは、この「確認して次を指示する」部分を仕組み化して、人を往復の外に出します。元記事が引用している Addy Osmani(Google Chrome チーム)の定義を借りれば、要は「自分自身を “エージェントに指示を打つ人” から外し、代わりに打ってくれる仕組みを設計する」ことです。人は最初に一度だけゴールを与え、あとは AI が「作る → 検査する → ダメなら直す」を自動で繰り返す。人の役割は、最後に結果(差分)を見るだけになります。
元記事は、ループを回す動作を 5 つに分解しています。
| 動作 | やること |
|---|---|
| 発見(discovery) | この 1 周で何をやるべきかを自分で見つける |
| 受け渡し(handoff) | タスクを隔離して、作業役に渡す |
| 検証(verification) | 別の関門が「これでいいか」を判定する |
| 記憶(persistence) | 状態を会話の外(ファイル等)に書き出す |
| スケジューリング(scheduling) | 放っておいても回り続けさせる |
この 5 つのうち心臓部は 検証(verification) です。AI が「できました」と言っても鵜呑みにせず、別の関門がテストで合否を判定し、緑になるまで終わらせない。この「ダメと言える評価役」があるかどうかが、ただの一発生成とループの分かれ目になります。本記事の FizzBuzz 環境は、この検証を厚く作り、残りの動作は最小限にした構成です。
なお元記事では、評価役を「作業役とは別のモデル」にするのが基本とされています。ただし FizzBuzz のように合否をコードで決定的に(True/False で)判定できるお題なら、判定役のモデルは不要です。テストコマンドそのものが、買収できない評価役になります。今回はこの決定的な検証を使います。
想定環境
- macOS / Linux / WSL(Windows の場合は WSL 上で行う)
- Node.js 18 以上(推奨 20 / 22)
- Claude Code CLI(フックとサブエージェントに対応したバージョン)
フック(hook): Claude Code の用語。エージェントの動作の特定タイミング(編集後・停止時など)で、指定したコマンドを自動実行させる仕組みのこと。
Windows ネイティブではなく WSL を使うのは、後述のフック用スクリプトが Unix 系シェル(bash)とそのコマンドを前提にしているためです。
全体像:何を用意し、何がループを回すのか
この練習環境は、次のファイルで構成されます。
| ファイル | 役割 | 対応する動作 |
|---|---|---|
CLAUDE.md |
ループの回し方と完了条件を定義 | (回し方の指示) |
.claude/settings.json |
フックと権限モードの設定 | (フックの配線) |
.claude/hooks/check.sh |
テストで合否を判定する評価役 | 検証 |
.claude/agents/fixer.md |
行き詰まり打破の別動エージェント | 受け渡し / 検証 |
src/fizzbuzz.ts |
実装対象のお題コード | (お題) |
src/fizzbuzz.test.ts |
合否を決める仕様兼テスト | (仕様) |
package.json |
test / typecheck コマンドの定義 | (実行設定) |
tsconfig.json |
型チェックの設定 | (実行設定) |
ループを実際に駆動している中心は 3 つです。CLAUDE.md(どう回すかの指示)、check.sh(緑になるまで止めない評価役)、fixer.md(詰まったときの打破役)。残りは、その上でループが働くための土台(お題・仕様・実行設定)です。元記事が既存プロジェクトで用意していた 3 ファイル(CLAUDE.md / settings.json / fixer.md)に、お題一式を足したものだと思ってください。
この構成は「テストが仕様も兼ねる」点が最小化のポイントです。FizzBuzz は正解が一意に決まるので、自然言語の仕様書を別に用意せず、テストファイルが「何を作るか」をそのまま表せます。仕様が曖昧だったり、見た目のように○×で測れない要素が入ると、この畳み方は使えず、別途の仕様書が必要になります。
セットアップ手順
以下を上から順に実行します。
1. プロジェクトの箱を作る
mkdir loop-practice && cd loop-practice
mkdir -p src .claude/agents .claude/hooks
2. 依存を入れる
npm init -y
npm i -D typescript vitest
3. package.json に検査コマンドを配線
package.json の scripts を次に書き換え、あわせて "type": "module" を足します。
"scripts": {
"test": "vitest run",
"typecheck": "tsc --noEmit"
}
4. tsconfig.json を作る
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "Bundler",
"strict": true,
"noEmit": true,
"skipLibCheck": true,
"types": ["vitest/globals"]
},
"include": ["src"]
}
5. お題とテストを作る
お題(src/fizzbuzz.ts)は、わざと未実装にしておきます。これがループの出発点(=テストが赤い状態)になります。
// 練習用のお題: この関数を tests が通るように実装する。
export function fizzbuzz(n: number): string {
throw new Error("not implemented");
}
テスト(src/fizzbuzz.test.ts)が「何を作るか」を定義します。
import { describe, it, expect } from "vitest";
import { fizzbuzz } from "./fizzbuzz";
describe("fizzbuzz", () => {
it("3で割れる数は Fizz", () => { expect(fizzbuzz(3)).toBe("Fizz"); });
it("5で割れる数は Buzz", () => { expect(fizzbuzz(5)).toBe("Buzz"); });
it("15で割れる数は FizzBuzz", () => { expect(fizzbuzz(15)).toBe("FizzBuzz"); });
it("それ以外はその数の文字列", () => { expect(fizzbuzz(7)).toBe("7"); });
});
6. .gitignore を作る
node_modules などを Git に含めないようにします。
node_modules/
.claude/.loop-count
7. CLAUDE.md(ループ協議)
「完了とは何か」を再定義します。ここが「直線ではなくループで進めろ」という指示の本体です。元記事の協議から linter を外し(今回は lint 設定を持たないため)、テストと型チェックの 2 つに絞っています。
## ループ協議
各タスクは「直線」ではなく「ループ」として走らせる:
1. 変更を書く
2. チェックを走らせる: テスト + 型チェック
3. 失敗した? エラーを読み、原因を特定し、直して、2 に戻る
4. ループは最大 5 回まで
停止条件:
- 全チェック通過 → 「完了」と報告。通過した出力を証拠として添える
- 5 回使い切った → 止まって、何が残っているか報告する
- 同じエラーが 2 回連続 → ループを止め、@fixer を呼ぶよう促す
禁止: チェック出力なしで「完了」と報告すること
禁止: アサーション削除やテスト弱体化で通すこと。直すのはコードで、スコアボードではない
最後の 2 つの「禁止」が、実はこの設定でいちばん効きます。停止条件(評価基準)を曖昧にすると、ループは平気でズルをします。元記事の著者も、これを入れずに回したら Claude がアサーションを 1 行こっそり消してテストを通した、と報告しています。テストは緑、でもバグは残ったまま——という事故を防ぐための一文です。
8. .claude/settings.json(フックと権限)
{
"permissions": {
"defaultMode": "acceptEdits"
},
"hooks": {
"Stop": [
{ "hooks": [ { "type": "command", "command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/check.sh\"" } ] }
],
"PostToolUse": [
{ "matcher": "Write|Edit", "hooks": [ { "type": "command", "command": "npx tsc --noEmit --pretty false 2>&1 | head -20" } ] }
]
}
}
-
Stopフックは、Claude が「完了」と言おうとするたびにcheck.shを実行します。これが元記事でいう 「ダメと言える評価役」 の本体で、ここが通らない限りループは終われません。 -
PostToolUseフックは、ファイルを編集するたびに型チェックを自動で走らせます。 -
defaultMode: "acceptEdits"は、ファイル編集の確認プロンプトを自動承認するモードです。
9. .claude/hooks/check.sh(評価役の本体)
#!/usr/bin/env bash
set -o pipefail
MAX_LOOPS=5
DIR="${CLAUDE_PROJECT_DIR:-.}"
STATE_FILE="$DIR/.claude/.loop-count"
cd "$DIR" || exit 0
OUT="$(npm test --silent 2>&1)"
CODE=$?
if [ "$CODE" -eq 0 ]; then rm -f "$STATE_FILE"; exit 0; fi
COUNT=0; [ -f "$STATE_FILE" ] && COUNT="$(cat "$STATE_FILE" 2>/dev/null || echo 0)"
COUNT=$((COUNT + 1)); echo "$COUNT" > "$STATE_FILE"
if [ "$COUNT" -gt "$MAX_LOOPS" ]; then
rm -f "$STATE_FILE"
echo "テストが赤のまま上限(${MAX_LOOPS}回)に到達。停止します。" 1>&2
exit 0
fi
{ echo "テスト失敗(${COUNT}/${MAX_LOOPS}回目)。緑にするまで完了にしないこと。"; printf '%s\n' "$OUT" | tail -20; } 1>&2
exit 2
作成後、実行権限を付けます。
chmod +x .claude/hooks/check.sh
このスクリプトが判定する分岐は 3 通りです。
- テストが緑 →
exit 0(停止を許可) - テストが赤 かつ 上限内 →
exit 2(もう 1 周させる) - テストが赤 だが 上限超過 →
exit 0(暴走させず打ち切り)
exit 2 が「まだ終わるな、もう一度やれ」の合図で、これがループを次の周へ押し戻す中核です。周回数は .loop-count に記録され、最大 5 回で打ち切ることで、直らない課題を延々と回してコストを浪費する「暴走」を防ぎます。これは 5 動作でいう 記憶(周回数の永続化) と、暴走への上限設定にあたります。
10. .claude/agents/fixer.md(行き詰まり打破役)
サブエージェント(sub-agent): Claude Code の用語。メインの会話とは別コンテキストで動く補助エージェント。失敗ログで埋まった思考をリセットし、白紙から診断させる用途に使う。
同じテストが直しても失敗し続けるとき、まっさらなコンテキストで診断させる別動エージェントです。CLAUDE.md の「同じエラーが 2 回連続 → @fixer を呼ぶ」から呼ばれます。
---
name: fixer
description: 同じテストが2回の修正後も失敗したときの、行き詰まり打破用エージェント
tools: Read, Edit, Grep, Glob, Bash
model: opus
---
あなたは失敗したチェックを直す。推測は禁止。
1. テストと型チェックを自分で実行しエラー全文を読む
2. 失敗パス上のファイルを頭から終わりまで読む
3. 一文で書く: 本当の原因は何か
4. その原因だけ直す。ついでのリファクタはしない
5. チェックを再実行し、修正前後の出力を報告する
禁止: テスト削除、アサーション緩和、エラー握りつぶし、skip化
直前の失敗の記憶を持たないままゼロから診断するので、一発で通ることがよくあります。これは元記事の 6 パーツでいう Sub-agents(生成役と評価役の分離) にあたります。FizzBuzz では 1 周で緑になることが多く、fixer はめったに発火しませんが、詰まったときの保険として置いておきます。
ループを回す
準備ができたら、出発点になっているかを確認します。
npm test # 4 failed になれば OK(=ループの出発点)
npx tsc --noEmit # エラーなしになれば OK
テストが赤・型チェックが緑なら準備完了です。Claude Code を起動し、こう頼みます。
src/fizzbuzz.ts を実装して。テストが全部通るまでループして。
すると、次のことが自動で起こります。
- Claude が
fizzbuzz.tsを編集する - 編集のたびに
PostToolUseフックが型チェックを走らせる - Claude が「完了」と言おうとすると、
Stopフック(check.sh)がテストを実行する - 赤なら
exit 2で編集に差し戻され、緑なら停止する - 緑になったら、あなたは差分(
/diff)を見るだけ
FizzBuzz のような単純なお題なら、多くの場合 1 周で緑に到達します。ここで消えるのは「ターミナルのエラーをコピペして貼り直す作業」です。Claude が自分で走らせ、自分で見て、自分で直す。あなたは往復の外に立ちます。
つまずきやすいポイント
許可確認が出て止まる
初期状態では、ファイル編集やコマンド実行のたびに確認プロンプトが出ます。この練習では defaultMode: "acceptEdits" で編集は自動承認していますが、Claude が自分で npm test などを実行しようとすると、別途確認が出ることがあります。
学習として体感するだけなら、都度承認して進めても問題ありません。確認をなくして完全に無人で回す設計は、セキュリティ(隔離環境や権限の絞り込み)を含む別テーマなので、慣れてから取り組むのがおすすめです。
check.sh の落とし穴(重要)
このスクリプトで最も大事なのは、テストの終了コードを、出力を切り詰める前に確保している点です。
OUT="$(npm test --silent 2>&1)" で出力を変数に取り、直後の CODE=$? で npm test 自身の終了コードを拾っています。これは単一コマンドの結果なので、合否は正しく取れます。
もしこれを避けて npm test | tail -20 のようにパイプの末尾で切り詰めると、set -o pipefail を付けない限り、パイプライン全体の成否が末尾の tail の成否(ほぼ常に成功)になり、テストが失敗しても「成功」と誤判定されます。すると評価役が機能せず、緑でもないのにループが止まります。合否の取得と出力の整形は分けて書く、と覚えておくと安全です。
まとめ
このスモールケースは、ループの構成要素のうち検証(check.sh)を厚く作り、発見と記憶を最小限に持たせ、受け渡しとスケジューリングは簡略化した構成です。
要点は次の 3 つです。
- 人は最初に一度だけ指示し、往復の外に立つ
- 評価役(Stop フック)が緑になるまで完了させない
- 周回に上限を設けて暴走を防ぐ
まずはこの最小構成で「AI が自分で赤を見つけて直す」感覚を掴み、そこから実プロジェクトや無人運用へ広げていくのが、無理のない入り口になります。概念の全体像や、Mastra を使った自作フレームワーク側の話、回しっぱなしの代価については、元記事 入門から実践 -「🔁 ループエンジニアリング」(@Syoitu) を参照してください。