Claude CodeでGraphQL Codegenを使う — 生成物を触らせないdenyとhook設定
Claude CodeでGraphQL Codegenを使うとき、生成ファイルの手編集をEdit denyで止め、スキーマ変更後の再生成をPostToolUse hookで自動化する設定をまとめます。
GraphQL Code Generator(通称graphql-codegen)は、GraphQLスキーマとクエリから型付きのコードを生成するツールです。Claude Codeに任せると、最初に起きやすい事故は生成ファイルの手編集です。型エラーを消すためにsrc/gql/の中身を直接書き換えられ、次の再生成で変更が消えます。
対策は3つの層に分けられます。
- 生成物への編集を
permissions.denyで止める - スキーマやクエリを変えた直後に、PostToolUse hookでcodegenを再実行する
- CLAUDE.mdに「正はスキーマ」という規約を書く
この記事では、公式ドキュメントの例に沿ったcodegen.tsを前提に、3層の設定を順に組みます。
正はスキーマ、生成物はその写し
GraphQL Codegenの入口はcodegen.tsです。CLIはこの設定ファイルを自動で見つけ、generatesに書かれた出力先へコードを書き出します。公式のReact向けガイドにある設定は、次の形です(例示であり、プロジェクトに合わせて書き換えます)。
import type { CodegenConfig } from '@graphql-codegen/cli'
const config: CodegenConfig = {
schema: 'https://graphql.org/graphql/',
documents: ['src/**/*.tsx'],
ignoreNoDocuments: true,
generates: {
'./src/gql/': {
preset: 'client'
}
}
}
export default configサーバー側の型生成は、typescriptとtypescript-resolversの2プラグインを./resolvers-types.tsに出力する構成が公式のApollo Server / GraphQL Yogaガイドに載っています。スキーマはschema.graphqlとして切り出し、package.jsonには"generate": "graphql-codegen"というスクリプトを置く流れです。
ここから、Claudeに触らせるものと触らせないものが決まります。
| 種類 | パスの例 | Claudeの扱い |
|---|---|---|
| スキーマ | パスの例schema.graphql | Claudeの扱い編集してよい。変更の起点 |
| クエリ・フラグメント | パスの例src/**/*.tsx内のGraphQL | Claudeの扱い編集してよい |
| 設定 | パスの例codegen.ts | Claudeの扱い編集してよい(変更は人が確認) |
| クライアント生成物 | パスの例src/gql/ | Claudeの扱い編集させない |
| リゾルバー型 | パスの例resolvers-types.ts | Claudeの扱い編集させない |
「生成物は読めるが書けない」状態が目標です。Claudeは生成された型を読んで実装を合わせる必要があるため、読み取りまで止めると作業が成り立ちません。
生成物への編集をdenyで止める
.claude/settings.jsonに、生成物のパスをEditの拒否ルールとして入れます。
{
"permissions": {
"deny": [
"Edit(/src/gql/**)",
"Edit(/resolvers-types.ts)"
]
}
}ポイントは4つあります。
パスの書き方
先頭が/のパターンは、設定ファイルの置き場所を基準にした相対パスです。プロジェクトの設定に書いたEdit(/src/gql/**)は、プロジェクトルート配下のsrc/gql/を指します。ファイルシステムのルートではありません。
Writeではなく、Editと書く
Claude Codeがファイル権限の判定に使うのはEdit(path)とRead(path)のルールだけです。Write(src/gql/**)のようなルールは受理されても参照されず、起動時に警告が出ます。ファイルを編集する組み込みツール全般がEditルールの対象になるため、Edit1本で足ります。
Readのdenyは使わない
Readのdenyルールは、同じパスへのEditとWriteも止めます。生成物を守る目的では一見便利ですが、Claudeが型定義を読めなくなります。読ませたいファイルにはEditのdenyを選びます。
codegen自体は動く
生成物を書き込むのは、npm run generateから起動されるcodegenのプロセスです。公式のPermissionsドキュメントによると、ReadとEditのdenyルールが効くのは組み込みのファイルツール、Bashの中で認識されるcat・sed・teeなどのファイルコマンド、>などのリダイレクト先です。NodeやPythonのスクリプトが自分でファイルを開いて書く動作には及びません。そのためdenyを入れても、再生成のコマンドは通ります。
PreToolUseで止める方法との違い
hooks-guideには、PreToolUseで編集前にスクリプトを走らせ、保護対象のパスなら終了コード2でブロックする例があります。例では.env・package-lock.json・.git/をパターンに並べ、ブロックの理由をstderrで返します。Claudeはその理由を読んで、やり方を変えられます。
使い分けはこうなります。denyは固定のパスを宣言するだけで済み、スクリプトもjqも要りません。PreToolUseのhookは、パスに加えて内容や状況でも分岐させたいときの選択肢です。src/gql/とresolvers-types.tsを丸ごと塞ぐだけなら、denyの2行で足ります。
overwriteの設定は保護にならない
codegen.tsにはoverwriteという項目があり、既定はtrueです。既存ファイルがあっても生成時に上書きします。公式の説明では、overwrite.updateExistingFilesで既存ファイルの上書き可否を、overwrite.removeStaleFilesでもう生成されない古いファイルの削除可否を別々に決められます。後者が効くのはwatchモードだけです。
生成物を守る目的でoverwriteをfalseにすると、次の再生成で最新のスキーマが反映されなくなります。手編集を防ぐのはdeny、最新に保つのはcodegenの上書き、と役割を分けておきます。
スキーマ変更後にcodegenを再実行するhook
denyだけでは、スキーマを直したあとに生成物が古いままになります。Claudeがスキーマやクエリを編集した直後にcodegenを走らせれば、型が最新の状態で次の作業に進めます。
PostToolUseイベントは、ツール呼び出しが成功したあとに発火します。Edit|Writeをマッチャーにすると、ファイル編集のあとだけ動きます。設定は次のとおりです。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/codegen.sh"
}
]
}
]
}
}.claude/hooks/codegen.shは、編集されたファイルがGraphQL関連のときだけcodegenを呼びます。hookの入力はJSONで標準入力に渡され、編集されたパスはtool_input.file_pathにあります。
#!/bin/bash
FILE=$(jq -r '.tool_input.file_path // empty')
# スキーマとクエリを書くファイルだけを対象にする
# *.tsx は codegen.ts の documents と揃える
case "$FILE" in
*.graphql|*.gql|*.tsx) ;;
*) exit 0 ;;
esac
cd "$CLAUDE_PROJECT_DIR" || exit 0
if ! OUT=$(npm run generate 2>&1); then
echo "$OUT" | tail -n 30 >&2
exit 2
fijqが必要です。CLAUDE_PROJECT_DIRは、hookのプロセスに環境変数として渡されます。
失敗をClaudeに返す
終了コード2は、hookが処理をブロックする合図です。PostToolUseでは、ツールがすでに実行済みなので取り消しにはなりません。代わりにstderrの内容がClaudeに渡ります。スキーマの記述ミスでcodegenが落ちたとき、その出力がClaudeに届き、次の修正に使われます。
終了コード0のhookが書いたstderrは、Claudeには見えません。失敗を知らせたいなら、exit 2にする必要があります。
実行回数を減らしたいとき
1回のタスクでスキーマを何度も直すと、そのたびにcodegenが走ります。生成に時間がかかる規模なら、Stopイベントにまとめて1回だけ実行する構成も選べます。Stopは、Claudeが応答を終えるたびに発火します。ファイルパスは渡されないので、拡張子での絞り込みは使えません。
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/codegen-once.sh"
}
]
}
]
}
}#!/bin/bash
INPUT=$(cat)
# 失敗を返したあとの再実行で無限に続けないための確認
if [ "$(echo "$INPUT" | jq -r '.stop_hook_active')" = "true" ]; then
exit 0
fi
cd "$CLAUDE_PROJECT_DIR" || exit 0
if ! OUT=$(npm run generate 2>&1); then
echo "$OUT" | tail -n 30 >&2
exit 2
fiStopフックが終了コード2を返すと、Claudeは止まらずに作業を続けます。stop_hook_activeの確認は、連続でブロックし続けないための公式の定石です。PostToolUseとStopの違いはClaude Code hooksでテストを自動実行するで扱っています。
整形やLintをhookに足したい場合も、同じ形が使えます。PostToolUse hookのLint自動修正の早見表が参考になります。
codegen側のhooksと取り違えない
GraphQL Codegenにも、hooksという設定項目があります。名前が同じなので紛らわしいのですが、持ち主も役割も別物です。
| Claude Codeのhooks | codegenのlifecycle hooks | |
|---|---|---|
| 設定場所 | Claude Codeのhooks.claude/settings.json | codegenのlifecycle hookscodegen.tsのhooks |
| 動くきっかけ | Claude CodeのhooksClaudeのツール呼び出し | codegenのlifecycle hookscodegenの内部イベント |
| 代表的な用途 | Claude Codeのhooks編集後にcodegenを起動する | codegenのlifecycle hooks生成後にPrettierをかける |
codegen側には、afterStart・onWatchTriggered・beforeOneFileWrite・afterOneFileWrite・afterAllFileWriteなどのイベントがあります。公式の例では、afterOneFileWrite: ['prettier --write']で生成ファイルを整形します。内容が前回から変わっていないファイルについては、afterOneFileWriteは発火しません。
生成物の整形はこちらに寄せるのが自然です。Claude Code側のhookでprettierを回そうとしても、src/gql/への編集はdenyで止まっているためです。
watchモードとの使い分け
codegenには--watchもあります。公式のReact / Vueガイドでは、@parcel/watcherを入れ、ignoreNoDocuments: trueを設定したうえでgraphql-codegen --watchを起動する手順です。設定のwatchは、スキーマの変更を検知して再生成するフラグで、配列で監視対象のglobを足せます。
watchは人間が編集する場面で便利です。Claudeの編集後に動かす用途では、hookの単発実行のほうが扱いやすい面があります。
- 失敗時の出力をstderr経由でClaudeに返せる
- 手編集された生成物も、実行のたびに書き直される
2つ目は、公式のcontentComparisonの説明から読み取れる点です。watchモードの既定値はcache-firstで、CLIは最後に書き出した内容のハッシュを覚えています。新しく生成した内容がそのハッシュと同じなら、書き込みをスキップします。生成物が外部から変更されていても、ディスクとの比較は行われません。'disk'を指定すればディスク上の内容と比べますが、手編集が残るリスクを避けたいなら、単発の実行と編集のdenyを組み合わせる構成が単純です。
CLAUDE.mdに書く規約
denyとhookは仕組みですが、Claudeが「なぜ触れないのか」を理解していると、無駄な試行が減ります。CLAUDE.mdには短く書いておきます。
## GraphQL
- 正はスキーマ(schema.graphql)とクエリ。src/gql/ と resolvers-types.ts は
graphql-codegen の生成物なので編集しない(編集は拒否される)
- スキーマやクエリを変えたら、フックが codegen を再実行する。
失敗したらそのエラーを読んでスキーマ側を直す
- 型エラーが出たら、生成物ではなく呼び出し側かスキーマを直す
- 変更後は `npx tsc --noEmit` で型を確認する最後の1行は、型の整合を実際のコマンドで確かめさせるためです。hookが再生成し、tscが型を検証するという流れで、スキーマ駆動の反復が1ターンの中で閉じます。
つまずきやすい点
スキーマの取得元がURLのとき
公式の設定例はschema: 'http://localhost:4000/graphql'のようにエンドポイントを指します。hookの実行時にそのサーバーへ届かない環境では、スキーマを取得できずnpm run generateが失敗します。hookを入れるなら、schema.graphqlのようなローカルファイルを正にする構成が安定します。
documentsのglobとhookの対象が食い違う
hookのスクリプトは拡張子でファイルを絞っています。codegen.tsのdocumentsをsrc/**/*.tsに変えたのにスクリプトが*.tsxのままだと、クエリを直しても再生成されません。2か所をセットで直す前提にします。
生成物の置き場所を変えたとき
generatesのパスを変えたら、denyルールも変えます。古いパスのルールが残り、新しい出力先は無防備、という状態になりがちです。
MCPで取るスキーマとの二重管理
GraphQL APIをMCPサーバー経由でClaudeに触らせている場合、Introspectionで得た最新のスキーマと、リポジトリのschema.graphqlがずれることがあります。MCPの設定はGraphQL MCPサーバーでClaudeにAPIを叩かせるにあります。コード生成の正をどちらに置くかは、先に決めておきます。
3層の役割
スキーマ駆動の運用で、3つの設定は別々の失敗を防いでいます。denyは「生成物を直された」、hookは「生成物が古い」、CLAUDE.mdは「Claudeが理由を知らず迷う」を扱います。どれか1つだけでは、残りの2つの失敗が起きます。まずdenyを入れ、次にhookを足すと、導入の効果が確かめやすくなります。