Claude CodeでOpenAPIをSpectralでlintする — 編集のたびに検証させる設定
OpenAPI定義の編集後にSpectralのlintを自動で走らせ、指摘をClaude Codeへ返す設定を、PostToolUse hookとCLAUDE.mdの書き方まで手順でまとめます。
OpenAPI定義をClaude Codeに編集させるなら、編集のたびにSpectralで検証させる構成が確実です。Spectralは、OpenAPIなどのJSON/YAMLをルールで検査するlinterです。ルールセットにチームの設計規約を書いておけば、Claude Codeの修正が規約に合っているかを機械が毎回判定します。
この記事では、PostToolUse hookでlintを自動実行し、指摘をClaude Codeへ返して直させるところまでを手順にします。CLAUDE.mdへ何を書くか、警告だけでは止まらない終了コードの罠も扱います。
Spectralでlintする3つの部品
Spectralは「ruleset(ルール集)」「CLI」「出力形式」の3つで動きます。まず役割を押さえます。
| 部品 | 役割 | 本記事での置き場所 |
|---|---|---|
| ルールセット | 役割どの項目をどう検査するかの定義 | 本記事での置き場所リポジトリ直下の.spectral.yaml |
| CLI | 役割spectral lintで定義を検査する | 本記事での置き場所@stoplight/spectral-cli |
| hook | 役割編集の直後にCLIを呼ぶ | 本記事での置き場所.claude/hooks/spectral-lint.sh |
Spectralは汎用のYAML/JSON linterで、ルールセットが無いと何も検査しません。プロジェクトのREADMEは、まず.spectral.yamlを作る手順を最初に置いています。ルールセットはJSON、YAML、JavaScript/TypeScriptのいずれかで書けます。
インストールとルールセットの作成
CLIはnpmでインストールします。プロジェクトに閉じる場合は-Dで開発依存にします。
npm install -D @stoplight/spectral-cli
npx spectral --versionSpectralにはspectral:oas(OpenAPI v2/v3)、spectral:asyncapi、spectral:arazzoの3つの組み込みルールセットがあります。OpenAPIだけを検査するなら、次の1行で足ります。
echo 'extends: ["spectral:oas"]' > .spectral.yaml
npx spectral lint openapi/openapi.yaml--rulesetを省略すると、CLIはカレントディレクトリの.spectral.yml、.spectral.yaml、.spectral.json、.spectral.jsのいずれかを探します。見つからなければ、そのドキュメントは検査されません。hookをプロジェクト外のディレクトリから呼ぶと、エラーにもならずlintが素通りする理由がここにあります。後述のhookでは--rulesetを明示します。
空のOpenAPIに何が指摘されるか
手元のSpectral 6.16.3で、infoにタイトルとバージョンだけを書いた最小のOpenAPI 3.0.3を検査すると、組み込みルールセットは次の警告を返しました。
1:1 warning oas3-api-servers OpenAPI "servers" must be present and non-empty array.
2:6 warning info-contact Info object must have "contact" object.
2:6 warning info-description Info "description" must be present and non-empty string.
7:9 warning operation-description Operation "description" must be present and non-empty string.
7:9 warning operation-tags Operation must have non-empty "tags" array.どれもwarnです。info-contactやoperation-descriptionのように、組み込みルールの多くは既定で有効かつwarnの重大度で報告されます。この事実が、次の終了コードの話につながります。
警告だけでは終了コードが0になる
hookでlintを自動化するとき、最初に踏む罠です。SpectralのCLIにはerror、warn、info、hintの4段階の重大度があります。既定ではerrorが1件でもあれば終了コード1で終わり、警告だけなら0で終わります。
| 実行 | 警告のみのときの終了コード |
|---|---|
spectral lint openapi.yaml | 警告のみのときの終了コード0 |
spectral lint openapi.yaml --fail-severity warn | 警告のみのときの終了コード1 |
上の表は、上で使った最小定義にパスの形とsummaryを直してerrorをゼロにした状態で、手元で実測した値です。終了コードを見るhookは、既定のままだと警告を全部見逃します。--fail-severity warnを付ければ、警告以上で失敗扱いになります。--display-only-failures(-D)を足すと、出力もその重大度以上に絞れます。
どこまでを失敗にするかは、ルールセット側でも調整できます。extendsしたルールの重大度だけを変える書き方が用意されています。
extends: ["spectral:oas"]
rules:
operation-success-response: error
info-contact: offrulesにルール名: 重大度だけを書くと、元のルールの定義を残したまま重大度を差し替えられます。offで無効化もできます。既存の定義に警告が数十件ある状態から始めるなら、まず問題の大きいルールだけをerrorに上げ、残りはwarnのまま段階的に直していくやり方が現実的です。
自分たちの設計規約をルールにする
組み込みルールが見るのは、主にOpenAPIとして破綻していないかどうかです。命名や必須項目といったチーム固有の規約は、自作ルールで書きます。ルールの基本形は、公式のREADMEに載っている「パスをkebab-caseにする」例のとおりです。
extends: ["spectral:oas"]
rules:
paths-kebab-case:
description: パスは kebab-case にする
message: "{{property}} は kebab-case にしてください"
severity: error
given: $.paths[*]~
then:
function: pattern
functionOptions:
match: "^(\\/|[a-z0-9-.]+|{[a-zA-Z0-9_]+})+$"
operation-summary-required:
description: 全オペレーションに summary を付ける
severity: error
given: $.paths[*][get,post,put,patch,delete]
then:
field: summary
function: truthygivenはJSONPathで検査の対象を選び、thenで関数を適用します。上のファイルで、パスが/userProfilesでオペレーションにsummaryが無い定義を検査すると、手元では次の2件がerrorで出ました。
6:17 error paths-kebab-case /userProfiles は kebab-case にしてください
7:9 error operation-summary-required 全オペレーションに summary を付けるここまでを組み合わせると、spectral:oasの警告が5件、自作ルールのエラーが2件の合計7件になります。operation-summary-requiredは、公式の例にあるpatternと同じ型で、組み込み関数のtruthyをsummaryフィールドに当てたものです。
Claude Codeの側から見ると、ルールは規約の文章よりも強い指示になります。CLAUDE.mdに「パスはkebab-case」と書くだけでは守られたかどうかが分かりません。ルールにすれば、守られていないときに具体的な行番号付きで返せます。
PostToolUse hookで編集のたびにlintを走らせる
Claude Codeのhookは、ツール呼び出しの前後にコマンドを走らせる仕組みです。PostToolUseはツールが成功した後に動き、matcherにEdit|Writeを指定すればファイル編集の直後だけに絞れます。コマンドには、編集対象のパスがJSONで標準入力から渡されます。
hookスクリプト
.claude/hooks/spectral-lint.shとして保存し、実行権限を付けます。
#!/bin/bash
FILE=$(jq -r '.tool_input.file_path // empty')
case "$FILE" in
*/openapi/*.yaml|*/openapi/*.yml|*/openapi.yaml) ;;
*) exit 0 ;;
esac
OUT=$(npx spectral lint "$FILE" \
--ruleset "$CLAUDE_PROJECT_DIR/.spectral.yaml" \
--fail-severity warn --display-only-failures 2>&1)
if [ $? -ne 0 ]; then
echo "$OUT" >&2
exit 2
fi要点は3つです。
- 対象のパスを
caseで絞り、OpenAPI以外のファイルは即exit 0で通す --rulesetを絶対パスで渡し、作業ディレクトリに左右されないようにする- 失敗したら標準エラー出力へlintの結果を出し、終了コード2で抜ける
最後の「終了コード2」が肝です。PostToolUseの時点でツールは実行済みなので、ブロックはできません。それでも終了コード2で終えると、標準エラー出力の内容がClaude Codeに見えます。終了コード0で終えたhookの標準エラー出力は、Claude Codeには見えません。lintの指摘をClaude Codeに直させたいなら、警告のみでも2で抜ける作りにします。
手元でスクリプトを直接動かして確かめました。警告が残るOpenAPIのパスを標準入力に渡すと終了コードは2、TypeScriptファイルのパスを渡すと0でした。
settings.jsonへの登録
プロジェクトの.claude/settings.jsonに登録します。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/spectral-lint.sh"
}
]
}
]
}
}登録後に/hooksを開くと、PostToolUseの下にフックが表示されます。動作確認は、Claude CodeにoperationIdを消すような編集をさせ、次の応答でlintの指摘を踏まえた修正が走るかどうかを見るのが早いです。
lintの自動修正を言語別に組む考え方は、PostToolUse hookのLint自動修正の早見表にまとまっています。ESLintのように自動修正を使えるlinterと違い、SpectralのCLIオプションに自動修正はありません。そのため「指摘を返してClaude Codeに直させる」流れになります。
CLAUDE.mdにルールセットの在りかを書く
hookは検査を強制しますが、「なぜ指摘されたか」「どの規約が正か」はClaude Codeの文脈にも入れておくと、直し方が安定します。公式のメモリ機能は、CLAUDE.mdに具体的で検証できる指示を書くこと、長くなる内容は分けることを勧めています。
## OpenAPI定義の編集
- 定義は`openapi/`配下のYAMLが正。編集後はhookがSpectralを実行する
- 規約は`.spectral.yaml`が正。指摘が出たら、ルールを緩めずに定義側を直す
- 新しい規約を足すときは`.spectral.yaml`にルールを追加し、理由を1行で残す
- `operationId`は`lower-hyphen-case`にする「ルールを緩めずに定義側を直す」の一行は実務で効きます。指示がないと、指摘を消す近道として.spectral.yamlのseverityをoffにする修正が混じりえます。ルールセットを触ってよい場面を、CLAUDE.mdで先に区切っておきます。
OpenAPI以外のコードが多いリポジトリでは、パス指定のルールに分けると文脈が節約できます。.claude/rules/openapi.mdの先頭にpathsを書くと、一致するファイルをRead、Write、Editしたときだけ読み込まれます。
---
paths:
- "openapi/**/*.yaml"
---
# OpenAPI編集のルール
- 指摘は`.spectral.yaml`に従って直す
- ルールの重大度を下げて指摘を消さないCLAUDE.mdは1ファイル200行以内を目安にするよう、メモリのドキュメントは書いています。OpenAPIの規約が長くなるなら、このパス指定ルールに移すのが筋です。
つまずきやすい点
運用に入ってから出やすい指摘を、原因と対処で並べます。
| 症状 | 原因 | 対処 |
|---|---|---|
| lintしても何も出ない | 原因ルールセットが見つからない | 対処--rulesetを明示する |
| 警告が多いのにhookが止まらない | 原因既定はerrorだけが失敗扱い | 対処--fail-severity warnを付ける |
oas3-unused-componentが誤検出する | 原因他の仕様から参照される部品集の仕様では、使われていない扱いになる | 対処該当ルールをwarnかoffにする |
外部ファイルへの$refが解決されない | 原因参照解決は別の設定が要る | 対処--resolverで解決方法を渡す |
oas3-unused-componentは、ルールの説明自体が「仕様を検査すると誤検出がありえる」と警告しています。共通部品を別ファイルに切り出している構成では、特に出やすいルールです。$refの解決は、独自のresolverをJavaScriptで渡す仕組みがCLIに用意されています。
出力形式を変えたいときは、-fで選びます。json、stylish、junit、html、sarif、github-actionsなどが使えます。複数の形式を同時に出す場合は、形式ごとに-oで出力先を指定します。Claude Codeに指摘を読ませるだけなら、既定のstylishで十分です。
CIでも同じルールを走らせる
hookはローカルのClaude Codeにしか効きません。人の手で直した変更や、別のツールが書いた変更は、CIで同じ.spectral.yamlを通します。
npx spectral lint "openapi/**/*.yaml" \
--fail-severity warn -f github-actionsgithub-actions形式はSpectral 6.10.0で導入されたもので、それより古いバージョンでは使えません。hookとCIで同じルールセットと同じ--fail-severityを使えば、手元で通ったものがCIで落ちる食い違いを避けられます。
OpenAPIを整えたあとの使い道としては、仕様からMCPサーバーを生成する方法があります。生成ツールは仕様の品質をそのままツールの品質にするため、lintでoperationIdや説明文が揃った定義は相性が良い入力です。手順はOpenAPI仕様からMCPサーバーを自動生成する3つの方法で扱っています。
まとめ
要点は、lintの指摘をClaude Codeに返す経路を作ることです。終了コード2で抜けるhookと、警告を失敗扱いにする--fail-severity warn、ルールの緩和を禁じるCLAUDE.mdの一行がそろえば、OpenAPIの編集は規約の中に収まりやすくなります。まずは既存の定義にspectral:oasだけをかけて指摘の量を見て、効かせたいルールだけをerrorに上げるところから始めるのが無理のない順序です。