Claude CodeでZigを書く — zig build testを完了条件にする
Claude CodeにZigを書かせるとき、zig build testを完了条件に固定するCLAUDE.md、permissions、Stop hookの設定例と、テスト出力の読み方をまとめます。
ZigをClaude Codeに任せるなら、完了の合図は「zig build test が成功した」に固定するのが近道です。Zigのコンパイルエラーにはファイル名・行・列が付き、テストの成否は終了コードに出ます。Claude Codeが出力を読んで直す反復に使える情報が、コマンド1つで返ってきます。
一方で、Zigは標準ライブラリのAPIが版ごとに変わります。ビルドシステム解説ページには、std.ArrayList(i32).init(...) を使ったサンプルが、0.17.0のコンパイラで has no member named 'init' のエラーになる実行結果がそのまま載っています。古い書き方のコードは、コンパイラが即座に弾くわけです。
この記事では、Zigの検証コマンドを CLAUDE.md、permissions、Stop hookの3層で固定する手順を扱います。Gradleの出力を絞る運用はClaude CodeでGradleビルドを回す、C++の検証コマンドの固定はClaude CodeでC++を開発するにあります。
検証対象になる最小のZigプロジェクトを作る
Zigはダウンロードページのアーカイブを展開してPATHに通すほか、パッケージマネージャーでも入ります。macOSのHomebrewなら最新のtagged releaseが入ります。
brew install zig
mkdir hello-world && cd hello-world
zig initzig init は build.zig、build.zig.zon、src/main.zig、src/root.zig の4ファイルを作ります。続けて zig build run を実行すると実行ファイルがビルドされて動き、最後に "Run zig build test to run the tests." と表示されます。雛形の段階で、テストの実行が zig build test に割り当てられているということです。
Zigのインストールは自己完結したアーカイブで、複数のバージョンを共存させられます。Claude Codeに作業させる前に zig version の出力を確認し、CLAUDE.md に書いておくと、後述するAPIの版ずれを疑うときの起点になります。
zig build testは何を実行しているのか
検証コマンドを固定するには、中身を知っておく必要があります。ビルドシステム解説によると、ビルドスクリプト経由の単体テストは、ビルドグラフの上で「Compileステップ」と「Runステップ」の2つに分かれます。
つまり、build.zig に b.addTest だけを書いて addRunArtifact を呼ばないと、テストはコンパイルされても実行されません。この2つをつなぐ依存辺がなければ単体テストは実行されないと、解説にも明記されています。
const unit_tests = b.addTest(.{
.root_module = b.createModule(.{
.root_source_file = b.path("main.zig"),
}),
});
const run_unit_tests = b.addRunArtifact(unit_tests);
const test_step = b.step("test", "Run unit tests");
test_step.dependOn(&run_unit_tests.step);Claude Codeに build.zig を触らせる場面では、この dependOn の1行が落ちていないかを確認します。テストが0件走って成功扱いになる状態は、完了条件としては何も守っていません。
単一ファイルだけを試すなら zig test foo.zig も使えます。ビルドスクリプトを通す zig build test との違いは、依存モジュールやターゲットの設定がビルドスクリプトに集約されている点です。プロジェクト全体の完了条件は zig build test、1ファイルの試行錯誤は zig test と分けると整理しやすくなります。
失敗の出力はどう見えるのか
Claude Codeが読むのは終了コードと出力です。Zigの出力で押さえておく形は3つあります。
コンパイルエラー
解説ページの失敗例では、エラー行にソースの該当行と位置を示す記号が付き、コンパイラが見た構造体の宣言位置が note: で添えられます。末尾は error: 1 compilation errors で、実行した zig test コマンド全体も failed command: として表示されます。ファイル名・行・列がそろっているため、Claude Codeは該当箇所を直接開けます。
テストの失敗
std.testing.expect(false) が失敗すると、FAIL (TestUnexpectedResult) に続いてエラーリターントレスが出ます。最後に "1 passed; 0 skipped; 1 failed." という集計が出て、コマンドは終了コード1で終わります。
テスト中のメモリリーク
std.testing.allocator を使うと、解放し忘れがテストの結果に反映されます。言語リファレンスの例では、defer list.deinit(gpa) を書き忘れたテストで、テスト自体は OK と表示されたあとに leaked のスタックトレースが出ます。続けて "All 1 tests passed." "1 errors were logged." "1 tests leaked memory." と並び、全体としては失敗扱いでコマンドが終了コード1になります。
「All 1 tests passed.」だけを拾うと成功に見えてしまうので、CLAUDE.md には「終了コードで判断する」と書いておきます。
完了条件をCLAUDE.mdに書く
CLAUDE.md は強制設定ではなく文脈として読まれます。そのため、メモリ解説は「検証できる程度に具体的に書く」ことを勧めています。「テストを確認する」ではなく「npm test を実行する」というレベルの具体性です。Zig向けに書くなら、次のような形が一例になります。
# Zig プロジェクトの規約
## Zigのバージョン
- 作業開始時に `zig version` を実行し、出力をこのファイルの値と比べる
- 標準ライブラリのAPIは版で変わる。コンパイルエラーが出たら、記憶ではなく
エラーメッセージと `lib/std` の該当ソースを読んで直す
## 完了条件
- 変更のたびに `zig build test --summary all` を実行する
- 判断は終了コードで行う。"All N tests passed." の1行だけで成功とみなさない
- 失敗したテストを `error.SkipZigTest` で飛ばして通さない
- テストの中で標準出力に書かない(ビルドランナーとの通信を壊す)2点目の「lib/std の該当ソースを読む」は、公式の記述ではなくこの記事の提案です。エラー出力が array_list.zig の宣言位置を note: で指すため、そこを手がかりにAPIの現行形を確認させる運用が成立します。
4点目は、ビルドシステム解説の記述に基づきます。ビルドランナーとテストランナーはstdinとstdoutでやり取りしながら複数のテストスイートを並行実行するため、テスト内の標準出力への書き込みは通信に干渉します。
3点目の SkipZigTest は、テストが error.SkipZigTest を返すとスキップ扱いになる仕様に対応しています。集計の skipped 件数を見れば検出できるので、「skippedが0であることを報告させる」と書き足してもかまいません。
permissionsで検証コマンドを許可する
毎回の承認プロンプトは、反復を遅くします。permissions解説では、Bash(npm run *) のようにワイルドカードで許可ルールを書けます。Zigの検証コマンドだけを通す設定は、たとえば次のようになります。
{
"permissions": {
"allow": [
"Bash(zig version)",
"Bash(zig build *)",
"Bash(zig test *)",
"Bash(zig fmt *)"
]
}
}zig build * は zig build run のようなプログラムの実行も含みます。検証専用に絞りたいなら、Bash(zig build test *) と Bash(zig build) を個別に書く方が範囲を狭くできます。
Stop hookでZigのテストを完了ゲートにする
CLAUDE.md の記述は守られないことがあります。確実に止めたいなら、Stop hookで zig build test を走らせます。hooksリファレンスでは、Stop hookが終了コード2で終わると「Claudeが止まるのを防ぎ、会話を続けさせる」と定められています。ブロック時のメッセージは、hookのstderrです。
#!/bin/bash
# .claude/hooks/zig-test-gate.sh
INPUT=$(cat)
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
exit 0 # 続行中の再入ではゲートを開ける
fi
if ! OUT=$(zig build test --summary failures 2>&1); then
echo "$OUT" | tail -n 60 >&2
exit 2
fiこれを settings.json に登録します。
{
"hooks": {
"Stop": [
{
"hooks": [
{ "type": "command", "command": ".claude/hooks/zig-test-gate.sh" }
]
}
]
}
}出力を tail -n 60 で切っているのは、エラーリターントレスやスタックトレースが長くなるためです。--summary には all、new、failures、line、none の5モードがあり、ここでは失敗したステップだけを出す failures を使っています。
stop_hook_active は、stop hookによってすでに続行している最中に true になります。hooksガイドは、収束しない条件でブロックし続けないよう、この値を見て早期に抜ける書き方を示しています。この例のとおりに書くと、再入した時点でゲートが開くため、修正のやり直しは1回分だけです。
修正に何度か反復が要りそうなら、このチェックを外し、連続ブロックの上限に任せる選択肢もあります。Claude Codeは、ツール呼び出しを挟まずにStop hookが連続して8回ブロックすると、そのターンを終了させます。この上限は環境変数 CLAUDE_CODE_STOP_HOOK_BLOCK_CAP で変えられます。仕組みはStop hookのブロック上限を調整する環境変数で扱っています。チェックを外すかどうかはこの記事の判断材料で、ガイドが示す書き方は早期終了のほうです。
PostToolUseとStopのどちらにテストを置くかは、テストの実行時間で決まります。使い分けの目安はClaude Code hooksでテストを自動実行するにあります。--test-timeout でテストの上限時間を決めておくと、無限ループしたテストでhookが固まる事態を避けられます。
つまずきやすい点
テストが走らない。 build.zig に test ステップがない、あるいは先述の dependOn が抜けている場合です。zig build --help を実行すると、ビルドスクリプトが公開しているステップが一覧に出ます。test が見えなければ、完了条件を出す以前の問題です。
クロスターゲットのテストが失敗する。解説ページの例のように複数ターゲットのテストをまとめて登録すると、ホストで動かせないバイナリも含まれます。Runステップは、ホストが実行できないときに実行を飛ばすよう設定でき、例では skip_foreign_checks = true を足しています。
途中経過の追跡が重い。長時間のセッションで再ビルドを繰り返すなら、zig build --watch でソースの変更を検知して再ビルドさせる手があります。ただし、Claude Codeが読む出力としては、一度の実行で完結する zig build test の方が終了コードで判断できるぶん単純です。
古いAPIで書いたコードが通らない。冒頭の ArrayList.init の例が典型です。同じZigの言語リファレンス(master版)では、リストは var list: std.ArrayList(u21) = .empty; で作り、list.append(gpa, '☔') のようにアロケーターを毎回渡す形で書かれています。作った時点のコードと、動かすコンパイラのバージョンが一致しているかを確認します。
進め方の手順
最初のセッションでは、機能より先に検証の経路を通します。
- 空のディレクトリで
zig initを実行させ、zig build testが成功するところまで確認する CLAUDE.mdにzig versionの出力と完了条件を書く- permissionsに
zig build *などを登録する - Stop hookを入れ、わざとテストを1つ失敗させて、Claude Codeが止まらず修正に進むことを確かめる
その後に機能を頼むと、変更のたびに同じ検証が走ります。Claude Codeのベストプラクティスも、テスト・ビルド・リンターのような「Claudeが実行して信号を読める検証」を与えることが、画面を見張る作業と席を外せる作業の差になると述べています。
まとめ
Zigでは、コンパイラが弾く古い書き方と、テストランナーが拾うメモリリークの両方が zig build test の終了コードに集約されます。完了条件を CLAUDE.md に書き、permissionsで承認の手間を減らし、Stop hookで止まる前に走らせれば、Claude Codeは自分でエラーを読んで直す反復に入れます。Zigのバージョンを先に確認し、build.zig の dependOn を点検することが、その土台です。