Claude Media
Claude Codeのactionlint活用 — workflowを編集のたびに検証する設定

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 actionlint

Goが入っていれば go install でも入ります。CI向けには、1コマンドで入るダウンロードスクリプトも用意されています。

go install github.com/rhysd/actionlint/cmd/actionlint@latest

Dockerイメージ(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 0
  • cd "$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
fi

1行目の 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: bash

shell: 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を入れる順序が無理のない進め方です。

この記事を共有:XはてブLinkedIn