Claude Codeのactionlint活用 — workflowを編集のたびに検証する設定
Claude Codeが書いたGitHub Actionsのworkflowを、編集のたびにactionlintで検査させる設定を手順でまとめます。PostToolUse hook、Stop hook、CLAUDE.mdの書き方までです。
Claude CodeにGitHub Actionsのworkflowを書かせるなら、編集のたびにactionlintを走らせる構成が手堅いです。actionlintは、workflowファイルを静的に検査するlinterです。キー名のtypo、${{ }} 式の型の誤り、存在しない入力名、注入されうる記述までを、実行前に指摘します。
YAMLとして正しくても、workflowとして動くとは限りません。この記事では、PostToolUse hookでactionlintを自動実行し、指摘をClaude Codeへ返して直させるところまでを手順にします。Bashで書き換えた場合の取りこぼしを拾うStop hookと、shellcheckの誤検出の扱いも扱います。
actionlintとhookとCLAUDE.mdの役割分担
actionlint自体はコマンド1本です。Claude Codeの側には、実行を強制する仕組みと、守るべき規約を伝える場所が要ります。
| 部品 | 役割 | 置き場所 |
|---|---|---|
| actionlint | 役割workflowを検査して終了コードで結果を返す | 置き場所ローカルにインストール |
| PostToolUse hook | 役割編集の直後にactionlintを呼び、指摘をClaude Codeへ返す | 置き場所.claude/hooks/actionlint.sh |
| Stop hook | 役割応答を終える前に全workflowを検査し直す | 置き場所.claude/hooks/actionlint-stop.sh |
| CLAUDE.md | 役割検査を黙らせてはいけない、というルールを書く | 置き場所リポジトリ直下 |
既存のGitHub Actionsのワークフロー作成をClaude Codeで進める手順は、生成したYAMLを人が読んで確認する流れでした。ここで足すのは、機械が先に落とす層です。
actionlintは何を見つけるのか
actionlintが検査する範囲は、構文だけではありません。READMEは次の特徴を挙げています。
| 検査 | 内容 |
|---|---|
| 構文 | 内容想定外のキーや不足しているキー |
| 式の型検査 | 内容${{ }} 内の存在しないプロパティや型の不一致 |
| actionの入力 | 内容with: の入力名と steps.<id>.outputs の正しさ |
| 再利用workflow | 内容呼び出し側と呼ばれる側のinputs / outputs / secrets |
| スクリプト | 内容run: へのshellcheckとpyflakesの適用 |
| セキュリティ | 内容信頼できない入力によるスクリプト注入、ハードコードされた認証情報 |
| その他 | 内容globの構文、needs: の依存、runnerラベル、cron構文 |
README掲載の壊れたworkflowの例では、linux-latest というラベルと、setup-node に渡す node_version(正しくは node-version)がそのまま指摘されています。
READMEの出力例から、指摘の形を2件引きます。
test.yaml:3:5: unexpected key "branch" for "push" section. expected one of "branches", "branches-ignore", "paths", "paths-ignore", "tags", "tags-ignore", "types", "workflows" [syntax-check]
test.yaml:21:20: property "platform" is not defined in object type {os: string} [expression]形式は ファイル:行:列: メッセージ [種別] です。行と列が付くので、Claude Codeは直す場所を探さずに済みます。末尾の種別(syntax-check や expression)は、後で -ignore で絞るときの手がかりになります。
actionlintをインストールする
方法は複数あります。macOSならHomebrewが手軽です。
brew install actionlintGoが入っていれば go install でも入ります。CI向けには、1コマンドで入るダウンロードスクリプトも用意されています。
go install github.com/rhysd/actionlint/cmd/actionlint@latestDockerイメージ(rhysd/actionlint)には、shellcheckとpyflakesが同梱されています。手元にshellcheckを入れたくないなら、これが近道です。
shellcheckは、PATH にあれば自動で使われます。無ければshellcheck連携が働かず、run: の中身は検査されません。run: 内のシェルスクリプトを見せたいなら、shellcheckも入れておきます。
手動で1回実行して、終了コードを確かめる
hookに組み込む前に、素のコマンドを理解しておきます。引数なしで実行すると、リポジトリ内のworkflowを見つけて検査します。ファイルを指定することもできます。
# リポジトリ内のworkflowをすべて検査
actionlint
# ファイルを指定して検査
actionlint .github/workflows/ci.yml終了コードは4種類です。
| 終了コード | 意味 |
|---|---|
| 0 | 意味正常終了。問題なし |
| 1 | 意味正常終了。問題あり |
| 2 | 意味コマンドラインオプションが不正 |
| 3 | 意味致命的なエラー |
指摘が1件でも見つかれば、終了コードは1です。hookでは「0以外はすべて失敗」と扱えば足ります。
PostToolUse hookで編集のたびに検査する
Claude Codeのhookは、ツール呼び出しの前後でコマンドを走らせる仕組みです。PostToolUse はツールが成功した直後に動き、matcher に Edit|Write を指定すれば、ファイル編集の後だけに絞れます。hookにはイベントの内容がJSONで標準入力に渡され、編集したファイルは tool_input.file_path で取れます。
hookスクリプト
.claude/hooks/actionlint.sh として保存します。
#!/bin/bash
FILE=$(jq -r '.tool_input.file_path // empty')
case "$FILE" in
*/.github/workflows/*.yml|*/.github/workflows/*.yaml) ;;
*) exit 0 ;;
esac
cd "$CLAUDE_PROJECT_DIR" || exit 0
OUT=$(actionlint "$FILE" 2>&1)
if [ $? -ne 0 ]; then
echo "$OUT" >&2
exit 2
fi保存したら実行権限を付けます。
chmod +x .claude/hooks/actionlint.sh要点は3つです。
caseで対象を.github/workflows/配下のYAMLに絞り、それ以外は即exit 0cd "$CLAUDE_PROJECT_DIR"でプロジェクトの根に移り、.github/actionlint.yamlの設定が効く位置で実行する- 失敗したら出力を標準エラーへ回し、終了コード2で抜ける
最後の「終了コード2」が肝です。PostToolUseの時点でツールは実行済みなので、止めることはできません。それでも終了コード2で終えると、標準エラー出力がClaude Codeに見えます。終了コード0では、出力は通常Claude Codeに渡りません。actionlintは問題があれば1を返すので、そのまま返さず2へ読み替えています。
settings.jsonへの登録
プロジェクトの .claude/settings.json に登録します。
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/actionlint.sh"
}
]
}
]
}
}登録後に /hooks を開くと、PostToolUse の下に表示されます。動作確認は、Claude Codeに「actions/setup-node の入力名をわざと node_version にしたworkflowを書いて」と頼み、次の応答で指摘を踏まえた修正が走るかを見るのが早いです。
hookの基本設計はPostToolUse hookのLint自動修正の早見表に言語別でまとまっています。actionlintには自動修正の機能が見当たらないため、「指摘を返して直させる」型になります。同じ型でOpenAPIを扱う例はClaude CodeでOpenAPIをSpectralでlintするにあります。
Stop hookで書き換えの取りこぼしを拾う
Edit|Write のmatcherが拾うのは、Claude Codeの編集ツールを通した変更だけです。Bash経由で sed や生成スクリプトがworkflowを書き換えた場合、PostToolUseの Edit|Write には掛かりません。
その穴は、応答を終える直前に動くStop hookで塞げます。Stop hookが終了コード2で抜けると、Claude Codeは止まらずに会話を続けます。
.claude/hooks/actionlint-stop.sh として保存します。
#!/bin/bash
ACTIVE=$(jq -r '.stop_hook_active // false')
[ "$ACTIVE" = "true" ] && exit 0
cd "$CLAUDE_PROJECT_DIR" || exit 0
[ -d .github/workflows ] || exit 0
OUT=$(actionlint 2>&1)
if [ $? -ne 0 ]; then
echo "$OUT" >&2
exit 2
fi1行目の stop_hook_active が重要です。Stopのhookには、すでにStop hookの結果を受けて続行中かを示す stop_hook_active が渡されます。これを見て抜けないと、直せない指摘があるときに同じ検査を延々と繰り返します。
登録は、Stop イベントにmatcherなしで足します。
{
"hooks": {
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/actionlint-stop.sh"
}
]
}
]
}
}PostToolUse側のhookと同じ hooks キーの下に並べて書きます。テスト実行でPostToolUseとStopを使い分ける考え方は、Claude Code hooksでテストを自動実行すると共通です。
CLAUDE.mdに検査のルールを書く
hookは強制力を持ちますが、Claude Codeが指摘を受けて何をするかは、CLAUDE.mdの書き方で変わります。次のような断片を置きます。
## GitHub Actions
- `.github/workflows/` のYAMLを編集すると、actionlintが自動で走る
- 指摘が出たら、workflow側を直す。`-ignore` や `.github/actionlint.yaml` の `ignore` で黙らせない
- 誤検出と判断したときは、理由を説明して人に確認する
- `run:` の中で `github.event.*.title` などを `${{ }}` で直接展開しない。`env:` に渡してから使う2行目と3行目が肝です。指摘を消す最短の手は、-ignore で握りつぶすことです。それを禁じ、誤検出だと考えるときは人に判断を戻させます。
4行目は、actionlintが検出する「スクリプト注入」の定石をあらかじめ伝えるものです。信頼できない入力(github.event.issue.title、github.event.pull_request.body、github.head_ref など)を run: に直接展開すると指摘されます。直し方は、env: に受けてからシェル変数として使う形です。公式の例に沿うと、次のような形になります。
- run: echo "issue ${TITLE}"
env:
TITLE: ${{ github.event.issue.title }}actionの with: に渡す分には問題ありません。公式の例にも、actions/stale の入力へ展開してよい旨が書かれています。ただし actions/github-script の script 入力は、スクリプトとして評価されるため、run: と同じく検査されます。
shellcheckの指摘と誤検出への対処
run: の指摘が増えると、一部は誤検出になります。actionlintはshellcheckを呼ぶとき、${{ }} をアンダースコアに置き換えてから渡します。それでも噛み合わない規則はあり、SC1091、SC2050、SC2194、SC2154、SC2157、SC2043は最初から無効化されています。
調整の手段は4つです。
| 手段 | 使いどころ |
|---|---|
shell: を明示する | 使いどころWindowsのjobで pwsh を使うのに、bashとして検査されるとき |
SHELLCHECK_OPTS 環境変数 | 使いどころshellcheckの規則を一部だけ除外・追加したいとき |
.github/actionlint.yaml の paths.<glob>.ignore | 使いどころ特定ファイルの特定メッセージだけを正規表現で除外したいとき |
-shellcheck= | 使いどころshellcheck連携ごと止めたいとき(速度は上がる) |
シェルを静的に決められないとき、actionlintは bash と仮定します。たとえば matrix.os で分岐する if: の中のPowerShellは、bashとして検査されて誤検出します。shell: pwsh を書けば避けられます。保守の面でも明示した方が読みやすいです。
規則を1つ除外する例です。
SHELLCHECK_OPTS='--exclude=SC2129' actionlint設定ファイルは -init-config で雛形を作れます。
actionlint -init-config自前のself-hostedランナーを使っていると、runs-on: のラベルが未知として指摘されます。そのときは、設定ファイルの self-hosted-runner.labels に自分のラベルを列挙します。config-variables に変数名を並べると、vars. の綴りまで厳密に検査できます。
self-hosted-runner:
labels:
- linux.2xlarge
config-variables:
- DEFAULT_RUNNER
paths:
.github/workflows/release.yaml:
ignore:
- 'the runner of ".+" action is too old to run on GitHub Actions'CLAUDE.mdで「ignoreで黙らせない」と書いた場合でも、ラベルの登録のように設定として正しい調整は別です。設定ファイルを変更するときは人のレビューを通す、と一言添えておくと運用が安定します。
CIでも同じ検査を走らせる
hookが効くのは、手元のClaude Codeだけです。人が直した変更や、別のツールが書いた変更には掛かりません。同じ検査をCIにも置くと、取りこぼしを減らせます。公式が示す最小の構成は、ダウンロードスクリプトで実行ファイルを取り、そのまま走らせるものです。
name: Lint GitHub Actions workflows
on: [push, pull_request]
jobs:
actionlint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Download actionlint
id: get_actionlint
run: bash <(curl https://raw.githubusercontent.com/rhysd/actionlint/main/scripts/download-actionlint.bash)
shell: bash
- name: Check workflow files
run: ${{ steps.get_actionlint.outputs.executable }} -color
shell: bashshell: bash は省けません。Windowsのrunnerでは既定のシェルが pwsh になるためです。shellcheckはUbuntuのworkerに最初から入っているので、Ubuntuで回す限り追加の準備は要りません。
CIの指摘をPRの注釈として見せたいなら、Problem Matchersかreviewdogを使う方法があります。hookとCIの両方で同じ .github/actionlint.yaml が読まれるため、手元で通ったものがCIで落ちる食い違いは起きにくくなります。
Claude Code自身をGitHub Actionsで動かす構成は、Claude CodeをGitHub Actionsに組み込むで扱っています。そのworkflowを書き換えるのもClaude Codeなら、ここで作ったhookがそのまま守りになります。
つまずきやすい点
| 症状 | 原因 | 対処 |
|---|---|---|
| hookが何も言わない | 原因actionlint が PATH に無い、またはスクリプトに実行権限が無い | 対処which actionlint と chmod +x を確認する |
run: の指摘が出ない | 原因shellcheckが無い、またはjobがWindowsで pwsh になっている | 対処shellcheckを入れる。shell: bash を明示する |
| 古いactionの指摘が出る | 原因actions/checkout@v3 のように、動作環境(runner)が古いactionを使っている | 対処新しいメジャー版へ上げる |
| 最新版でないのに指摘されない | 原因actionlintは「最新版か」を検査しない | 対処DependabotでactionのバージョンをPRとして追う |
| Stop hookが止まらない | 原因stop_hook_active を見ていない | 対処スクリプト冒頭で true なら exit 0 にする |
| Bashで書いた変更に反応しない | 原因EditとWriteのmatcherはBashを拾わない | 対処Stop hookを併用する |
古いactionの検出は、READMEの「outdated popular actions」の項に例があります。actions/checkout@v3 がnode16のrunnerを使うため古いと指摘されます。ただし、これは「実行環境が現役でない」ことを検出するもので、「最新版ではない」ことの検出ではありません。最新版の追従は別の仕組みに任せます。
まとめ
要点は、actionlintの指摘をClaude Codeに返す経路を作ることです。終了コード2で抜けるPostToolUse hook、取りこぼしを拾うStop hook、そして「黙らせない」と書いたCLAUDE.mdの3つがそろえば、workflowの編集は実行前に機械の検査を通ります。
まずは既存のworkflowに actionlint を1回かけ、指摘の量を見ます。設定で調整すべきものを .github/actionlint.yaml に寄せたうえで、hookを入れる順序が無理のない進め方です。