はじめに
TalentXエンジニアリング部部長の岸本です。エンジニア全体のミッションやプロジェクトの管理などを担当しています。
私たちはGolang(以下 Go)を採用したバックエンドの開発にClaude Codeを導入しています。運用する中で、「コード生成コマンドを実行しない」問題が繰り返し発生しました。 はじめはCLAUDE.mdに記載していましたが無視されることがあり、スキルに書く方法も試したうえで、最終的に hookで強制するかたちに落ち着きました。その過程で「どこに書けば守られるのか」の判断基準がはっきりしたので、Goの特徴を踏まえつつ、実装上の工夫とあわせてまとめます。
Goの強み:コード生成
コード生成を活用しやすいエコシステム
私たちのあるアプリケーションでは以下のようなコード生成が動いています。
| 生成器 | 入力 → 出力 |
|---|---|
| OpenAPI Generator | OpenAPIスキーマ → サーバの型・ハンドラ定義 |
| ORMのモデル生成 | DBスキーマ → モデル構造体 |
| Mockery | interface → テスト用モック |
| google/wire | プロバイダ定義 → DI初期化コード |
| マイグレーションツール | SQLファイル → DBスキーマ |
Goではリフレクションを多用したメタプログラミングより、「生成して物理ファイルにする」アプローチのほうが広く使われています。DIは実行時解決ではなく依存解決用のコードを生成し、モックも実行時プロキシではなくファイルとして生成されます。
おかげでエンドポイントを 1 本増やすときに手で書くのはほぼビジネスロジックだけで、型もDIの配線もモックも自動で揃うという点は、明確にGoの強みです。
コード生成がされないという課題
何が起きたか
この構成のリポジトリで実装をエージェントに任せ始めると、次のことが繰り返し起きました。
- OpenAPIスキーマを編集して実装まで書き上げるのに、生成コマンドを踏まない
- マイグレーションファイルを追加したのに、モデルを再生成しない
この結果CIでの失敗が増え、人間が確認するという手間が増えたのが課題感です。 この課題を以下のフローを検証し解決していきました。
- ガイドライン
- スキル
- hook
段 1:ガイドライン
まずリポジトリのガイドライン(CLAUDE.md)に「OpenAPIスキーマを編集したら生成コマンドを実行してください」などと書く方法があります。
効くこともありますが確実ではありません。ガイドラインは要するにプロンプトの一部なので、モデルがどう重み付けするかに委ねられています。コンテキストが長いとき、指示項目が多いとき、タスクが複雑なときに落ちやすいという傾向があります。
とはいえこの層が不要になることはありません。「どのレイヤーに何を置くか」「命名をどう揃えるか」といった判断を要する指針は、ここ以外に書き場所がないからです。
段 2:スキル
次に、チケット起点の実装フローをスキルとして切り出し、その手順の中に生成コマンドを埋め込みました。例えば「実装が終わったら、テストを書く前にモック生成と DI再生成を実行する」というように、手順の文脈の中に位置づける ことを試みました。単独の注意書きではなく、やるべきことの並びの一項目として現れるため、ガイドラインより有効でした。
ただし構造的な限界があります。スキルの数だけ同じことを書く必要があり、レビュー用の別スキルなどには効きません。記述が重複するのでメンテナンスコストもかかります。実際、生成コマンドの呼び出し方をMakefile ターゲット経由に統一したとき、複数スキルの記述を横断して直す作業が発生しました。
また明示的な指定なしではスキルの呼び出しは Claude Code次第となるため、強制力といった点では不十分です。
段 3:hook
そこでhookに注目しました。Claude Codeのhookは、エージェントがツールを使う前後に任意のコマンドを差し込む仕組みです。ファイル編集やコマンド実行の前後(PreToolUse / PostToolUse)に、対象ファイルやコマンド文字列の条件付きで自前のスクリプトを実行できます。エージェントの判断とは無関係に、設定した条件が満たされれば必ず動きます。
以下が設定の例です。
| タイミング | 条件 | 実行するもの |
|---|---|---|
| PostToolUse | OpenAPIスキーマの編集 | スキーマからのコード生成 |
| PostToolUse | マイグレーションの実行 | モデル再生成 |
利点は 2 つ。どの経路から作業されても発火すること(スキル経由でも素の対話でも、ファイルパスとコマンド文字列がトリガーなので関係ない)と、書く場所が 1 箇所に集約されることです。
判断基準は「機械で判定できるか」

| 強制力 | 書く場所 | 書けるもの | |
|---|---|---|---|
| ガイドライン | 低(確率的) | 1 箇所 | 判断・方針・命名・レイヤー配置 |
| スキル | 中(手順の文脈で効く) | スキルごとに重複 | 順序・承認ゲート・停止条件 |
| hook | 高(決定論的) | 1 箇所 | 機械判定できる条件のみ |
基準はシンプルで、機械で判定できるものはhook。生成コマンドの実行、lint、テストは判定できます。「このロジックはドメイン層に置くべきか」は判定できないので、上の層に残ります。
hookにする上で工夫した点
hookは設定ファイルに数行書けば動きますが、実運用に耐える状態にするまでにいくつも躓きました。
1. 「何を守るか」より先に「いつ守るか」を決める
最初の間違いから書きます。マイグレーション実行後のモデル再生成を、当初 PreToolUse に置いていました。つまり make migrate が走る前に make generate-model を実行していたのです。DBスキーマが変わる前にモデルを生成しても、出てくるのは変更前のモデルであったため、PostToolUse に移して解決しました。
基本的な観点になりますが、hookを設計するときは「何を守るか」より先に「いつ守るか」を決めるべきです。生成は変更の後、検査は送信の前というような順序を固定してから条件式を書くほうが早いです。
2. 宣言的マッチャを信用せず、スクリプトで二重検証する
hookの設定には対象コマンドを絞る条件式があり、Bash(git push *) のように宣言的に書けます。見た目は気持ちがいいのですが、複合コマンドや引用符が絡むと条件を正しく解釈できず、誤って発火することがありました。パースが複雑すぎる場合に安全側へ倒れる挙動のようです。
これが実害を生みます。git push を含まないコマンドでpush用のhookが発火すれば、実行時間が無駄になってしまいます。逆に発火してほしい場面で発火しなければ、生成漏れが素通りします。発火するかどうかが読めないhookは、無いのと大差ありません。

そこで条件判定を自前スクリプトに持たせ、hookからはそれを呼んで「本当にそのコマンドなのか」を再検証する二段構えにしました。標準入力からJSONでツールの情報が渡るので、実際のコマンド文字列を取り出します。
TOOL_NAME="$(printf '%s' "$INPUT" | jq -r '.tool_name // empty')" ACTUAL_COMMAND="$(printf '%s' "$INPUT" | jq -r '.tool_input.command // empty')"
そしてコマンド境界でのみマッチさせます。
# 境界 = 行頭 もしくは ; && || | ( の直後
# 直後 = 空白 / セパレータ / ) / 行末 のいずれか
REGEX="(^|[;&|(][[:space:]]*)[[:space:]]*${ESCAPED_PREFIX}([[:space:]]|;|&|\||\)|\$)"
if printf '%s' "$ACTUAL_COMMAND" | grep -Eq "$REGEX"; then
exec /bin/bash -c "$COMMAND_TO_RUN"
fi
前方の境界に ( を含めるのは $(...) のようなコマンド置換に対応するため。後方も空白やセパレータに限定するのは、git pushall のような別コマンドを誤って拾わないためです。単純な部分文字列一致では、この 2 つを取りこぼします。
プレフィックスは正規表現に埋め込むため、メタ文字をエスケープしてから使います。ここは外部コマンドを呼ばず、シェルのパラメータ展開だけで処理しました。hookはファイル編集やコマンド実行のたびに走るので、sed の起動コストを考慮しています。
escape_ere() {
local s="$1"
s="${s//\\/\\\\}" # バックスラッシュを必ず最初に
s="${s//./\\.}"; s="${s//\*/\\*}"
# 以下 ^ $ ( ) + ? { } | [ ] も同様に置換
printf '%s' "$s"
}
順序が重要です。バックスラッシュを最初に処理しないと、後続の置換が挿入したバックスラッシュを二重にエスケープしてしまいます。また引数のプレフィックスは末尾のワイルドカード(git push *)を許容し、内部で剥がしています。設定ファイル側の書き方と見た目を揃えたかったからです。
実行対象は /bin/bash -c に渡すので lint && test のような複合コマンドをそのまま指定できます。なおこの形に落ち着くまでには回り道があり、当初はもっと込み入った方式で書いていたものを、レビューでの指摘を受けて「宣言的な条件式 + 検証ラッパー」に寄せました。
3. 観測できないhookは、壊れていても気づけない
hookはエージェントの外側で動きます。つまり発火したかどうかを誰も観測していません。発火したのか、発火した上で何か問題があるのか、という点を観測できず原因を特定できない点に課題がありました。
そこで全スクリプトにログを持たせました。重要なのは、成功だけでなくスキップの理由を区別して記録することです。条件に一致しなかったのか、対象ツールではなかったのか、コマンドが空だったのか。分けておくと「発火していない」と気づいたときに原因が即座に分かり、誤発火が起きていないかも後から追えます。
まとめ
Goの強みである「コード生成」を活かすためにhookの実用例・使い所を紹介しました。git操作に関しては今までgit hookなどがありましたが、人間の運用で特定のファイルを変更した時に必ず実行していたコマンドなどがあれば、ガイドラインやスキルなどに書くより効果的です。
最後に
TalentXでは、一緒にAI駆動開発を推進できるメンバーを募集しています!AIエージェントを利用したモダンな開発フローに興味がある、推し進めたい、という方はぜひこちらのリンクからどうぞ!