Claude CodeでCheckovを使いTerraformをスキャンする手順
Checkovの指摘をClaude Codeに直させ、checkov:skipや--skip-checkで握りつぶされないように止める手順。CLAUDE.mdの規約、deny設定、hookの例つき。
CheckovをClaude Codeに回させるときの難所は、スキャンの実行ではなく「指摘の消し方」です。放っておくと、Terraformの該当リソースに checkov:skip のコメントを足すだけで結果がきれいになります。直させるのは設定の中身で、コメントでも --skip-check でもありません。
この記事では、Checkovの実行と結果の渡し方、直させる範囲のCLAUDE.md、握りつぶしを止めるdenyとhook、再スキャンで閉じる流れを順に扱います。Terraformを書く段階の検証(fmt、validate、plan差分)はClaude CodeでTerraformのコードを書く手順にあり、ここでは静的スキャンに絞ります。
Checkovで何が分かるか
Checkovは pip install checkov で入るIaCの静的スキャナーです。checkov -d <ディレクトリ> でフォルダ内のTerraformなどを調べ、チェックごとにPASSED / FAILED / SKIPPEDを出します。
pip install checkov
checkov -d infraQuick Startの例では、S3バケットの acl を public-read にしたときに、結果の先頭が Passed checks: 3, Failed checks: 1, Skipped checks: 0 になります。失敗したチェックは S3 Bucket has an ACL defined which allows public access. で、リソース名(aws_s3_bucket.foo-bucket)とファイル名、行範囲(/example.tf:1-25)、該当コードが並びます。
Claude Codeに渡すうえで大事なのは、次の3点です。
- 失敗したチェックのID(
CKV_AWS_20のような名前)がリソース単位で分かる - 行範囲が付くので、直す場所をそのまま開ける
- サマリー行の
Skipped checksで、握りつぶされた件数を数えられる
3点目が、この記事の後半で効いてきます。
結果をClaude Codeに読ませる
CLI出力は --quiet で失敗したチェックだけに絞れ、--compact でコードブロックを省けます。長いスキャン結果をそのまま渡すと文脈を食うので、まずこの2つを付けます。
checkov -d infra --quiet --compact出力形式は -o で選べ、json、junitxml、sarif などがあります。ファイルに残して読ませたいときは、標準出力をリダイレクトします。
checkov -d infra --quiet -o json > checkov.json--output-file-path は出力先の「フォルダ」を指定するオプションなので、ファイル名を決めたいときはリダイレクトのほうが確実です。
外部モジュールを使う構成では、--download-external-modules true を付けるとモジュールのソースが .external_modules に取得され、その中身もスキャンされます。保存先は --external-modules-download-path で変えられます。
checkov -d infra --download-external-modules true --quiet --compact.external_modules は取得物なので、Claude Codeに直させる対象ではありません。後述のdenyで編集を止めます。
指摘を直させるCLAUDE.md
直し方の方針は、スキャンの前にCLAUDE.mdで決めます。スキャンのたびにプロンプトへ書くより、規約として固定したほうがぶれません。
## Checkov(Terraform)
- スキャン: `checkov -d infra --quiet --compact`
- FAILED は Terraform の設定を直して解消する
- `checkov:skip` コメントの追加、`--skip-check`、`--soft-fail`、
baseline の作成は行わない
- 直せない指摘は、チェックIDとリソース名の一覧にして報告し、止める
- 修正後は同じコマンドで再スキャンし、FAILED が 0 になるまで続ける
(3 回直しても残るなら止める)
- 再スキャンのサマリーで `Skipped checks` が増えていないことを確認する実際の修正は、指摘の種類で変わります。先ほどのS3の例なら、公式のコンプライアントな例どおり acl = "private" に戻すのが直し方です。暗号化、アクセスログ、バージョニングの指摘であれば、足りないブロック(server_side_encryption_configuration や versioning)を追加する形になります。
プロンプトで頼むときは、範囲を限定します。
checkov.json の FAILED を、infra 配下の .tf を直して解消してください。
- .external_modules と .terraform は触らない
- 直した指摘は「チェックID / リソース / 変更内容」の表にする
- 直せないものは直さず、理由つきで一覧にするskipの追加をdenyする
Checkovには、指摘を消す道が3本あります。どれも「直した」ことにはならず、検出対象から外しているだけです。
| 経路 | 書き方 | 結果の見え方 |
|---|---|---|
| インラインコメント | 書き方リソースのブロック内に checkov:skip=<チェックID>:<理由> | 結果の見え方該当チェックがSKIPPEDになり、理由が出る |
| 実行時フラグ | 書き方--skip-check にID・ワイルドカード・重大度を指定 | 結果の見え方そのチェックは実行されず、出力にも現れない |
| baseline | 書き方--create-baseline で .checkov.baseline を作り、--baseline で比較 | 結果の見え方既知の指摘は新規の失敗として報告されない |
注意したいのは2行目です。公式の説明では、--skip-check で外したチェックは「まったく実行されず、出力にも現れない」とされています。コメントによるskipはSKIPPEDとして理由つきで残りますが、フラグによるskipは痕跡が残りません。--soft-fail(常に終了コード0を返す)も同様に、判定を無効にします。
コメントによるskipの結果は、公式の例では次のように出ます。リソース内に #checkov:skip=CKV_AWS_20:The bucket is a public static content host を足した場合です。
Check: "S3 Bucket has an ACL defined which allows public access."
SKIPPED for resource: aws_s3_bucket.foo-bucket
Suppress comment: The bucket is a public static content host
File: /example_skip_acl.tf:1-25FAILEDがSKIPPEDに変わり、Suppress comment: に理由が出ます。つまり、コメントによるskipは「見ればわかる」握りつぶしです。サマリーの Skipped checks を毎回見る運用は、このためにあります。
実行時の経路はpermissionsで止める
フラグ側は、.claude/settings.json のdenyルールで止めます。
{
"permissions": {
"deny": [
"Bash(checkov * --skip-check*)",
"Bash(checkov * --soft-fail*)",
"Bash(checkov * -s *)",
"Bash(checkov * --create-baseline*)",
"Bash(checkov * --baseline*)",
"Edit(.checkov.baseline)",
"Edit(.checkov.yaml)",
"Edit(**/.external_modules/**)"
]
}
}Edit(...) の拒否は、ファイルを編集するすべての組み込みツールに適用されます。.checkov.yaml は、--config-file で渡す設定ファイルの置き場所の例です。自分のリポジトリで使っているパスに合わせます。
ただし、Bashのルールで引数を絞る方法は壊れやすいと公式が注意しています。オプションの順序が変わる、--skip-check=CKV_AWS_20 のように = でつなぐ、変数を挟む、といった書き方は、パターンに掛からないことがあります。ここで止まらなかった場合の網が、次のhookです。
ファイルへの書き込みはhookで止める
コメントのskipは、コマンドではなくファイル編集で入ります。denyルールはパスを見るだけで中身を見られないので、PreToolUse のhookで書き込む内容を調べます。EditとWriteのツール入力には、置き換え後の文字列(new_string)と書き込む内容(content)が入っています。
#!/usr/bin/env bash
# .claude/hooks/block-checkov-skip.sh
input=$(cat)
added=$(echo "$input" | jq -r '.tool_input.new_string // .tool_input.content // ""')
removed=$(echo "$input" | jq -r '.tool_input.old_string // ""')
if echo "$added" | grep -q 'checkov:skip' \
&& ! echo "$removed" | grep -q 'checkov:skip'; then
jq -n '{hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: "deny",
permissionDecisionReason: "checkov:skip の追加は禁止です。設定を直すか、例外が必要な指摘を人に報告してください。"
}}'
fi
exit 0old_string に既に checkov:skip がある編集は通します。すでに承認済みのskip行の近くを直しても、止まらないようにするためです。
登録は settings.json の hooks で、マッチャーを Edit|Write にします。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/block-checkov-skip.sh"
}
]
}
]
}
}hookが拒否したときの理由はClaude Codeに返るので、「直せないなら報告して止める」というCLAUDE.mdの方針と合わせると、別の抜け道を探しにいかず、報告で終わりやすくなります。hookの基本はフックの仕組みの解説にあります。
CIで最後の網をかける
hookが守るのはClaude Codeのファイル編集ツールだけです。Bash経由の sed -i のような書き換えは通り抜けます。そのため、PRの差分に checkov:skip が新規に入っていないかをCIで確かめます。
if git diff origin/main...HEAD -- '*.tf' | grep -E '^\+.*checkov:skip'; then
echo "checkov:skip が追加されています。レビューで例外理由を確認してください"
exit 1
fi例外が本当に必要な指摘(公式の例なら、公開用の静的コンテンツを置くバケット)は、人がレビューを通してコメントを足します。理由欄(<suppression_comment>)は任意項目ですが、レビューで残す前提なら必須にしておくと、あとから経緯を追えます。
既存の指摘が大量にあるリポジトリでは
導入直前のリポジトリには、直しきれない既存の指摘が山ほど残っていることがあります。そのときだけは、--create-baseline が正当な道具になります。実行すると指摘の現状が .checkov.baseline に保存され、--baseline を付けた以降の実行では、baselineに無い新しい失敗だけが報告されます。--output-baseline-as-skipped を付ければ、baselineで見逃された項目もSKIPPEDとして出力に残せます。
ここで決めるのは役割分担です。baselineの作成と更新は人が行い、Claude Codeには「新しく出た失敗を直す」仕事だけを渡します。前節のdenyに --create-baseline と .checkov.baseline の編集を入れてあるのは、この分担を守るためです。人がbaselineを作ったあとは、--baseline .checkov.baseline をCLAUDE.mdのスキャンコマンドに足します。
checkov -d infra --baseline .checkov.baseline --quiet --compactbaselineがあると Failed checks: 0 の意味が「既存を除いて新規なし」に変わります。既存分の解消は別のタスクとして管理し、スキャンの合否と混ぜないようにします。
再スキャンで閉じる
直したあとは、同じコマンドで再スキャンして結果を見ます。見るのは次の2点です。
Failed checksが0になったことSkipped checksが直す前から増えていないこと
2点目を欠くと、skipを足してFAILEDを0にした状態と区別がつきません。CLAUDE.mdにこの確認を入れてあるのはそのためです。
終了コードで判定したいときは、--soft-fail を付けずに回します。このオプションは「チェックは実行するが常に終了コード0を返す」ものなので、付けるとCIでもClaude Codeの確認でも、失敗が失敗として現れません。特定のチェックだけ警告扱いにしたい場合は、--soft-fail-on と --hard-fail-on でID・重大度ごとに指定できますが、それも人が決める設定として、Claude Codeには触らせない側に置きます。
使いどころの切り分け
CheckovとTrivyはどちらもTerraformの設定ミスを調べられます。同じ流れをTrivyで組む場合はTrivyのスキャン修正ループにまとめています。並べると、この記事の型の違いは次の点です。
| 観点 | この記事(Checkov) | Trivyの記事 |
|---|---|---|
| 対象 | この記事(Checkov)IaC(Terraformなど)の設定ミス | Trivyの記事イメージ・依存関係・IaC |
| 例外の置き場所 | この記事(Checkov)リソース内の checkov:skip コメント | Trivyの記事専用の無視ファイル |
| 握りつぶしの止め方 | この記事(Checkov)コメントの追加をhookで止める | Trivyの記事無視ファイルの変更を確認する |
例外がコードの中に入る形式のCheckovでは、無視ファイルを守るだけでは足りません。ファイル編集の中身を見るhookが要るのは、この性質のためです。
よくあるつまずき
- SKIPPEDが増えているのに気づかない: 結果の先頭行だけでなく、
Skipped checksの数も見る。CLAUDE.mdに「増えていないこと」を書いておく --checkで範囲を絞って満足する:--checkは指定したチェックだけを実行するので、絞った範囲の外の失敗は見えない。範囲を絞る設定も人が決める- モジュールの中身が検査されていない: 外部モジュールは
--download-external-modules trueを付けないと展開されない。スキャン対象を変えたら、件数の変化を確認する - 取得物を直そうとして時間を使う:
.external_modulesの指摘は、モジュール側の問題で、手元のコードの修正では消えない。編集をdenyにして、報告に回す
まとめ
Checkovの指摘を直させる設計の核心は、直す道を設定の修正に一本化し、消す道(コメント、フラグ、baseline)を塞ぐことです。フラグ側はpermissionsのdeny、コメント側は編集内容を見るhook、そしてCIの差分チェックの三段で、どれか一つが抜けても残りが止めます。例外が必要な指摘は、Claude Codeが報告し、人が理由つきでコメントを足す。この役割分担にすると、スキャン結果の「0件」が、直した結果を意味するようになります。