Claude CodeでgovulncheckのGo脆弱性を読み、呼んでいるものだけ更新する
govulncheckの出力をClaude Codeに読ませ、呼び出し経路のある脆弱性だけを最小のgo getで更新する手順です。再スキャンをhookで自動化するところまで書きます。
Goの依存関係を脆弱性スキャンにかけると、警告の中に「自分のコードが呼んでいないもの」が混ざります。govulncheckは、そこを分けて出してくれるツールです。
この出力をClaude Codeに読ませると、更新すべき依存の仕分けと、go get の範囲決めまでを任せられます。ここでは、呼び出し経路のある脆弱性だけを最小の更新で閉じる手順と、更新のたびに再スキャンさせるhookの設定を書きます。
govulncheckは「入っている」と「呼んでいる」を分けて出す
govulncheckは、Goの既知の脆弱性を調べるツールです。依存関係を照会するだけでなく、静的解析で、自分のコードから脆弱な関数まで呼び出し経路があるかを見ます。結果は2つに分かれます。
- 呼び出し経路がある脆弱性:
Call stacks in your codeとして、呼び出し元の行つきで出る - 経路がない脆弱性:
Informationalの節にまとまり、「対応が要らないかもしれない」と注記される
Goのチュートリアルの出力例では、golang.org/x/text のv0.3.5を使うプロジェクトで、2件のうち1件だけが呼び出し経路つきでした。その行は main.go:12:29: vuln.tutorial.main calls golang.org/x/text/language.Parse の形です。もう1件のGO-2022-1059は、モジュールは同じでも、コードが脆弱な関数を直接にも間接にも呼んでいないため、Informationalに回っています。
同じモジュールの2件が別々に扱われる
GO-2021-0113
コードが language.Parse を呼ぶ。信頼できない入力を処理するなら、サービス拒否の入口になり得る。修正版はv0.3.7。
GO-2022-1059
コードは脆弱な関数を呼ばない。対応の優先度は下がる。修正版はv0.3.8。
npmやPythonのスキャナーは、依存関係の一覧を照合するだけで、コードの呼び方は見ません。その仕分けをClaude Codeに任せる手順はnpm auditとpip-auditの記事にあります。Goでは、その一番手間のかかる部分をツール側が先にやってくれます。
結果を保存して、読み取りだけ許可する
Claude Codeに渡す前に、結果をファイルに残します。スキャンは読み取り専用の操作なので、実行を許可しても依存は書き換わりません。
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./... > govulncheck.txt
govulncheck -show traces ./... > govulncheck-traces.txt-show traces を付けると、脆弱な関数までの呼び出しを、スタックの全段で出力します。通常の出力は呼び出し経路の要約で、main.go:12:29: ... calls ... の1行です。テストコードも対象にしたいときは -test、ビルドタグで有効になるファイルを含めたいときは -tags を渡します。
終了コードにも癖があります。脆弱性があれば0以外で終わりますが、-json と -format sarif、-format openvex のときは、件数にかかわらず0で終わります。終了コードで判定したいスクリプトやhookは、テキスト出力のままにします。
読み取りだけを許可する設定は、.claude/settings.json にこう書けます。
{
"permissions": {
"allow": [
"Bash(govulncheck *)",
"Bash(go mod why *)",
"Bash(go list *)",
"Bash(go test *)"
]
}
}go get は許可に入れていません。go.modを書き換える操作は、実行前に毎回確認を挟む運用にします。権限ルールの書式と優先順位は/permissionsコマンドの記事で整理しています。
呼び出し経路つきの出力を、更新の優先度に変える
次に、Claude Codeへ仕分けを頼みます。ファイルを変更させず、表だけを出させるのがポイントです。
govulncheck.txt と govulncheck-traces.txt を読み、脆弱性ごとに表にしてください。
列は ID / モジュール / Found in / Fixed in / 呼び出し経路の有無 / 呼び出し元のファイルと行 です。
- 経路がある脆弱性は、呼び出し元のコードを開き、入力が外部から来るかを書いてください。
- Informational は、同じモジュールの経路ありの脆弱性と同時に直せるかだけ書いてください。
ファイルは変更しないでください。例えば、次のような表になります(形の一例です)。
| ID | モジュール | Found in | Fixed in | 経路 | 区分 |
|---|---|---|---|---|---|
| GO-2021-0113 | モジュールgolang.org/x/text | Found inv0.3.5 | Fixed inv0.3.7 | 経路あり(main.go:12) | 区分1直す |
| GO-2022-1059 | モジュールgolang.org/x/text | Found inv0.3.5 | Fixed inv0.3.8 | 経路なし | 区分2同時に直せる |
区分は「1経路があるので直す」「2経路はないが、同じモジュールを上げるついでに閉じられる」「3経路もなく、修正版が遠いので記録だけ残す」の3つで足ります。チュートリアルの判断も同じ順です。経路のある脆弱性を先に見て、同じモジュールのInformationalは、修正版が近ければ一緒に閉じています。
「経路なし」は仮説として扱う
govulncheckの解析には限界があります。関数ポインタやインターフェース越しの呼び出しは、保守的に解析されるので、経路が実際より多く出ることがあります。反対に、reflect パッケージ経由の呼び出しは静的解析に見えません。そこだけから届く脆弱なコードは、ソースの解析では報告されません。unsafe を使うコードでは、見落としが出る可能性もあります。
そこで、Claude Codeの依頼文には、一言足しておきます。
経路なしの脆弱性のうち、reflect や unsafe を使うパッケージ経由で
呼ばれていそうなものは、表に「要手動確認」と書いてください。見つかった「要手動確認」は、go mod why -m <モジュール> で、なぜそのモジュールが必要かを確かめます。このコマンドは、メインモジュールからそのモジュールのパッケージまでの最短のimport経路を出します。
go getの範囲を最小にする
ここが、この手順の肝です。go get -u ./... は、コマンドラインで指定したパッケージが直接にも間接にもimportするモジュールを、すべて最新版まで上げます。脆弱な1つのモジュールを閉じたいだけなのに、関係のない依存まで動きます。
govulncheckの出力には Fixed in が書いてあるので、更新はそのモジュールだけに絞れます。
go get golang.org/x/text@v0.3.8
govulncheck ./...チュートリアルでは、v0.3.7が修正版のGO-2021-0113と、v0.3.8が修正版のGO-2022-1059を、v0.3.8への更新で一度に閉じています。同じモジュールに複数の脆弱性があるときは、Fixed in のうち一番新しいものを選ぶと、1回で済みます。
更新のコマンドは、広さの順に並べると選びやすくなります。
| 更新したい範囲 | コマンドの形 |
|---|---|
| 脆弱なモジュールだけ、修正版に | コマンドの形go get <モジュール>@<Fixed in> |
| 依存のうち、パッチ版だけ上げる | コマンドの形go get -u=patch ./... |
| 依存を全部、最新版に | コマンドの形go get -u ./... |
-u=patch は、各依存を最新のパッチ版までに限って上げます。-u patch ではなく、イコールでつなぎます。Claude Codeには、先頭の行のコマンドを使うよう指定します。
区分1と2だけを対象に、Fixed in の最新版を指定して go get <モジュール>@<版> を実行してください。
go get -u ./... は使わないでください。
その後 govulncheck ./... と go test ./... を実行し、結果を報告してください。Informationalが残るのは、修正版が出ていないか、更新してもテストが通らないときです。そのときは、区分3として理由を書いて残します。govulncheckには、検出結果を黙らせる機能がありません。無視した脆弱性は、出力に毎回出続けます。理由は、リポジトリのメモ側に書き留めます。
更新のたびに再スキャンさせるhook
手順の最後は「同じコマンドをもう一度回して、件数が減ったことで閉じる」です。これを毎回頼むのではなく、go get が成功したら自動で再スキャンするhookにできます。
使うのは PostToolUse イベントです。ツール呼び出しが成功した後に走ります。if に権限ルールの書式を書くと、go get のときだけ動かせます。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(go get *)",
"command": ".claude/hooks/govulncheck-after-get.sh"
}
]
}
]
}
}スクリプトは、スキャンが失敗したときに、結果を標準エラー出力に書いて終了コード2で終わります。PostToolUse の終了コード2は、stderrをClaudeに見せる動作になります。
#!/bin/bash
# .claude/hooks/govulncheck-after-get.sh
OUT=$(govulncheck ./... 2>&1)
STATUS=$?
if [ $STATUS -ne 0 ]; then
echo "$OUT" >&2 # stderr が Claude へのフィードバックになる
exit 2
fi
exit 0実行権限(chmod +x)を忘れないでください。PostToolUse のhookは、すでに実行されたツール呼び出しを取り消せません。そのため、このhookの役割は止めることではなく、更新後の状態をClaudeに突きつけることです。スキャンがまだ失敗していれば、Claudeはその出力を読み、次の更新に進みます。
なお、公式の終了コードの説明は「脆弱性があれば失敗」とだけあり、Informationalしか残らないときの扱いは書かれていません。Informationalだけでhookが毎回2を返すと、Claudeがそれを追いかけて無用な更新を始めます。手元のプロジェクトで一度確かめ、その場合は何もしないようスクリプトを直します。
CLAUDE.mdに仕分けのルールを書く
依頼文を毎回書かなくて済むよう、規約を CLAUDE.md に置いておきます。
## Goの依存関係の脆弱性対応
- スキャンは `govulncheck ./...`(テキスト出力)。`-json` は終了コードが0になるので判定に使わない
- 経路ありの脆弱性だけを直す。更新は `go get <モジュール>@<Fixed in>` に限る
- `go get -u ./...` は使わない。必要なら確認を取る
- 更新後は govulncheck と go test を実行し、結果を報告する
- 経路なしで残すものは、ID・理由・確認日を表で報告するここまでの流れは、Trivyで再スキャンして閉じる手順と同じ型です。Goのコードの静的検査を同じ流れに載せるなら、golangci-lintのhook設定も参考になります。
つまずきやすい点
落とし穴
標準ライブラリの脆弱性が出る
使っているGoのバージョンが古いと、標準ライブラリの脆弱性が追加で出ます。チュートリアルも、最新のGoを使っていなければ、標準ライブラリの脆弱性が見えることがあると注記しています。依存のgo getでは閉じないので、Go本体の更新が必要です。
もう1点は、コンパイル済みのバイナリを検査するときです。-mode binary を付けると、バイナリのシンボル情報から脆弱な関数を探します。ただし、バイナリには詳細な呼び出し情報が残らないので、呼び出し経路は出ません。実際には呼ばれていないコードまで報告される場合もあります。「経路があるものだけ直す」という今回の仕分けは、ソースの解析でだけ成り立ちます。
Claude Codeに読ませる場合も、バイナリの結果には同じ判断を適用できません。依頼文に「経路の列が空なのは、経路なしではなく、情報がないため」と書き添えておくと、表の誤読を防げます。
まとめ
govulncheckは、呼び出し経路のある脆弱性とない脆弱性を、出力の段階で分けてくれます。Claude Codeには、その出力を表にして、呼び出し元を開いて入力の出どころを確かめるところまでを任せます。
更新は、Fixed in で指定したモジュールだけに絞った go get で済みます。依存の全面更新は不要で、再スキャンをhookに置いておけば、閉じたかどうかを毎回の依頼なしで確かめられます。経路なしの判断は仮説なので、reflect や unsafe が絡む箇所だけは、人の目で見てください。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。