Claude Code の .claude 設定を静的検証する Linter「agentlint」を作った話

Gradient purple-pink hero banner with 'SIOS TECH LAB' in top-left and Japanese title about Claude Code and Linter on the left; decorative starburst on the right.

はじめに

こんにちは!サイオステクノロジーのなーがです。最近は Claude Code の Skill や Subagent を育てるのがすっかり日課になっていて、気づけば .claude/ 配下のファイルがかなりの数に膨れ上がってきました。

ただ、増えてくると困るのが設定の記述ミスです。SKILL.md のフロントマターのキーを typo した、Skill の説明文からリンクしていたファイルをリネームして参照切れになった、settings.json の hooks でイベント名を間違えた……。

こうしたミスの厄介なところは、実行するまで気付けないことです。しかも Claude Code は壊れた設定をエラーで教えてくれるとは限らず、該当の Skill や Hook を黙って無視することがあります。「あれ、この Skill 最近発動してなくない?」と気付いた頃には、いつのコミットで壊れたのか分からない……なんてことも。

これはもう Linter の出番だなということで、.claude/ 配下をコミット前に静的検証する agentlint という Linter を自作しました。今回はそのご紹介です。

agentlint とは

agentlint は、Claude Code のエージェント設定(.claude/ 配下の skills / commands / agents / hooks 設定)を検証する Python 製の Linter です。フロントマターの記述ミス、壊れたファイル参照、hooks 設定の構造ミスをコミット前に静的検出し、pre-commit や CI に組み込めるようにしています。

検証対象のファイルは以下の通りです。

your-project/
├── CLAUDE.md                  # 参照切れ検出
└── .claude/
    ├── skills/**/SKILL.md     # フロントマター検証 + 参照切れ検出
    ├── commands/**/*.md       # フロントマター検証 + 参照切れ検出
    ├── agents/*.md            # フロントマター検証 + 参照切れ検出 + 再委譲禁止記述の検証
    ├── settings.json          # hooks設定検証
    └── settings.local.json    # hooks設定検証

4つのチェック

チェック内容は大きく4つに分かれています。

チェック対象概要
1. フロントマター検証.claude/skills/**/SKILL.md.claude/commands/**/*.md.claude/agents/*.mdYAML フロントマターの構文・必須キー・列挙値・型を検証
2. 参照切れ検出上記 Markdown 本文 + ルートの CLAUDE.md$CLAUDE_PROJECT_DIR / $CLAUDE_SKILL_DIR / .claude/ 起点の参照、および本文中の相対パス参照の実在確認(すべて warning)
3. hooks 設定検証.claude/settings.json.claude/settings.local.jsonトップレベル hooks の構造・イベント名・matcher・handler、および command が参照するスクリプトの実在(AL305)を検証
4. サブエージェント再委譲禁止の検証.claude/agents/*.md本文に「他のサブエージェントを呼び出さない」等の再委譲禁止の記述があるかを検証(AL401、回帰防止)

4つ目だけ少し毛色が違いますが、これは私のプロジェクトで「サブエージェントがさらに別のサブエージェントを呼び出す多段リレーを禁止し、その旨を各エージェント定義の本文にも明記する」という運用をしているため、その記述が抜け落ちたときに警告してくれる回帰防止用のチェックです。この多段リレーを hooks で機械的に防ぐ話は別の記事にまとめているので、AL401 の背景が気になる方はあわせてどうぞ。なお、検証対象の一つである Agent Skills の仕組みそのものについては、弊社メンバーのブログ記事で紹介しているので、あわせて読んでいただけると理解が深まると思います。

なお、フロントマターの有効値リスト(イベント名、model / effort / permissionMode の列挙値など)は、公式ドキュメントを出典としてデータ専用のモジュール(src/agentlint/spec.py)に切り出してあり、仕様変更時はこのファイルだけを更新すればよい作りにしています。

参照: Claude Code Hooks – 公式ドキュメント

finding コード一覧

検出結果(finding)にはコードを振っています。AL1xx がフロントマター、AL2xx が参照・ファイルシステム、AL3xx が settings/hooks、AL4xx が運用ルール系です。

コード重大度内容
AL001warningagentlint 自身の内部エラー(ツールのバグでコミットをブロックしないための最終防波堤)
AL101errorフロントマターの YAML がパース不能
AL102error必須キー欠落(agent の name / description)
AL103error値が無効(model / effort / permissionMode 等の列挙違反、または列挙キーの値が文字列でない)
AL104warning未知のキー(「もしかして」候補付き)
AL105error型違反(bool / str / list / int 等、フロントマターのキーが文字列でない場合も含む)
AL106warningdescription 欠落(SKILL.md のみ。commands では任意のため対象外)、または description + when_to_use 合計が1536文字超
AL201warningアンカー付きパス参照切れ(Markdown 本文中)
AL202warning相対パス参照が見つからない(Markdown 本文中)
AL203warninghooks が直接実行するスクリプトに実行権限がない
AL301errorsettings JSON がパース不能
AL302error存在しないイベント名(「もしかして」候補付き)
AL303errormatcher の正規表現が不正
AL304error構造違反(配列でない、type が未知等)
AL305errorhooks の command が参照するスクリプトが存在しない(スクリプトパスと確信できるトークンのみ判定対象)
AL306warningmatcher 非対応イベントへの matcher 指定、未知キー
AL401warningエージェント定義本文に再委譲禁止(「他のサブエージェントを呼び出さない」等)の記述がない

出力形式

出力は ruff 風の1行1finding形式です。ファイルパス:行番号: コード [重大度] メッセージ の並びで、最後にサマリー行が付きます。

.claude/agents/foo.md:3: AL103 [error] 'model' の値が無効: 'gpt-4'
agentlint: 1 error(s), 0 warning(s)

問題がなければこうなります。

agentlint: ok (12 files checked)

設計思想: 誤検知ゼロを最優先

このツールを作るうえで一番こだわったのが、誤検知(false positive)を出さないことです。

pre-commit に組み込む Linter は、誤検知が1件でも起きると「またこれか」とチーム内で無効化・放置されてしまい、それ以降の見逃しの方が遥かに高コストになります。そこで agentlint では 「error にするなら warning 以上に保守的に。迷ったら検出しない」 を設計原則にしました。

error を2種類に限定した理由

コミットをブロックする error は、次の2種類だけに限定しています。

  1. フロントマター / settings の構文・構造エラー(AL101 / AL102 / AL103 / AL105 / AL301 / AL302 / AL303 / AL304): YAML や JSON としてそもそも壊れている、必須キーがない、列挙値が無効、型が違う、など機械的に白黒つけられるもの
  2. hooks の command が参照するスクリプトの実在確認(AL305): 「スクリプトパスだと確信できるトークン」だけに絞った実在確認

一方で、Markdown 本文中の参照切れ(AL201 / AL202)は常に warning です。Skill や Agent の説明文には .claude/skills/my-skill/SKILL.md のような例示パスが頻出し、プレースホルダ判定だけでは実在するパスと原理的に区別できないためです。本文中の参照切れでコミットを直接ブロックすることはしません。

未知のキー(AL104)も同様に warning に留めています。公式ドキュメントの更新で新しいキーが追加されたとき、agentlint 側の追従が遅れると誤検知になってしまうためです。

参照切れ検出そのものも「迷ったら検出しない」方針で、プレースホルダらしき文字列(path/toexampleyour-my- を含む等)や、絶対パス、URL(スキーム付き・裸ドメインの両方)、ワイルドカードを含むトークンは対象外にしています。

AL305 のスクリプトパス判定の工夫

error に昇格させた AL305(hooks のスクリプト実在確認)は、その分だけ判定を慎重にしています。というのも、hooks の command 文字列には「/ を含むけどパスではない」トークンが山ほど出てくるんですよね。例えば……

  • sed -i 's/foo/bar/g' — sed の置換パターン
  • jq -r ".a/b" — jq のフィルタ
  • rm -rf *.log — glob
  • date +%Y/%m/%d — 日付フォーマット
  • $HOME/... — 未解決のシェル変数

これらを素朴に「パスっぽいから実在確認しよう」とやると誤検知まみれになります。そこで agentlint では、$CLAUDE_PROJECT_DIR 置換後、未解決の変数($)や glob(* ? {})を含まず、.sh / .py 等の既知の実行系拡張子で終わる」トークンだけを実在確認の対象にしています(この判定は src/agentlint/pathtokens.py に共通化しています)。

さらに、引数位置(2番目以降)のトークンは、先頭トークンがインタープリタ / ランナー(bash / python / uv / node 等)の場合のみ対象にしています。これは cp src.sh dst.sh の宛先のような「実行対象ではない引数パス」を誤検知しないための対策です。

AL001: 自身のバグでコミットをブロックしない

もうひとつの防波堤が AL001 です。agentlint 自身のバグで想定外の例外が起きた場合、そのファイルの検査は諦めて AL001 の warning として報告し、他のファイルの検査は継続します。

Linter のバグでユーザーのコミットがブロックされるのは、体験として本当に最悪なんですよね。なので「ツールが壊れても error にはしない」を仕組みとして保証しています。チェック処理は1ファイル単位で例外を捕捉するラッパー越しに実行しているので、1ファイルで転んでも残りのファイルの検査結果はちゃんと出ます。

使い方

ここからは実際の使い方です。ローカル実行 → pre-commit → CI の順に組み込んでいきます。

インストールと実行

agentlint は GitHub で公開しています。PyPI などのパッケージレジストリには出していないので、リポジトリを clone して uv 経由で実行する形になります。

git clone https://github.com/Shotaro-Yoshinaga-sti/agentlint
cd agentlint && uv sync

セットアップできたら、あとは検証したいプロジェクトを --root で指定して実行するだけです。

uv run agentlint                    # カレントディレクトリの .claude/ を検証
uv run agentlint --root ../other    # 別ディレクトリを指定
uv run agentlint --strict           # warningのみでもexit code 1にする
uv run agentlint --version

例えば、こんな設定ミスを仕込んだサンプルの .claude/ を用意してみます。

  • agents/foo.md: 必須キーの name / description が欠落、model: gpt-4(無効な値)、再委譲禁止の記述なし
  • skills/deploy/SKILL.md: descriptiondescripton と typo、本文から存在しない ./checklist.md を参照
  • settings.json: hooks のイベント名を PreToolUses と typo、存在しないスクリプト .claude/hooks/check.sh を command で参照

これに対して実行すると、以下の出力になります(実際の実行結果です)。

uv run agentlint --root ../broken-example
.claude/agents/foo.md:1: AL102 [error] 必須キー 'description' が欠落している
.claude/agents/foo.md:1: AL102 [error] 必須キー 'name' が欠落している
.claude/agents/foo.md:1: AL401 [warning] サブエージェントの再委譲禁止(「他のサブエージェントを呼び出さない」等)の記述が見当たらない
.claude/agents/foo.md:2: AL103 [error] 'model' の値が無効: 'gpt-4'
.claude/settings.json:3: AL302 [error] 未知のイベント名 'PreToolUses'(もしかして: PreToolUse)
.claude/settings.json:3: AL305 [error] hooks が参照するスクリプトが存在しない: .claude/hooks/check.sh
.claude/skills/deploy/SKILL.md:1: AL106 [warning] description が設定されていない
.claude/skills/deploy/SKILL.md:3: AL104 [warning] 未知のキー 'descripton'(もしかして: description)
.claude/skills/deploy/SKILL.md:6: AL202 [warning] 相対パス参照が見つからない: ./checklist.md
agentlint: 5 error(s), 4 warning(s)

typo には「もしかして」候補が付くので、修正もすぐ終わります。これが地味に嬉しいんですよね。

exit code は error があれば 1、warning のみなら 0 です。CI で warning も落としたい場合は --strict を付けると warning のみでも exit code 1 になります。

また、.claude/ ディレクトリが存在しない場合は何もせず exit code 0 で終了します。モノレポの一部ディレクトリなど、対象外の場所で実行されても邪魔をしません。

agentlint: .claude ディレクトリが見つかりません(/path/to/other/.claude)。何もしません。

pre-commit への組み込み

agentlint は pre-commit hook としての利用を想定していて、リポジトリに .pre-commit-hooks.yaml を同梱しています。

公開しているので、利用側の .pre-commit-config.yaml にリポジトリを直接指定できます。

# .pre-commit-config.yaml
- repo: https://github.com/Shotaro-Yoshinaga-sti/agentlint
  rev: v0.2.0
  hooks:
    - id: agentlint

手元で改造しながら試したいときは、pre-commit try-repo でローカルのチェックアウトを直接指定するのが手軽です。

pre-commit try-repo ../agentlint agentlint --all-files

hook 定義側で files: ^(\.claude/|CLAUDE\.md) を指定してあるので、.claude/ 配下か CLAUDE.md に変更があったコミットのときだけ動きます。

参照: pre-commit 公式ドキュメント

CI での利用

pre-commit をすり抜けたケース(--no-verify でのコミットなど)に備えて、CI でも同じ検証を回しておくと安心です。GitHub Actions なら以下のようなジョブになります。uvx --from git+... で公開リポジトリから直接取得して実行するので、事前インストールは不要です。

# .github/workflows/agentlint.yml
name: agentlint
on: [push, pull_request]

jobs:
  agentlint:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: astral-sh/setup-uv@v5
      - name: Run agentlint
        run: uvx --from git+https://github.com/Shotaro-Yoshinaga-sti/agentlint agentlint --strict

ローカルの pre-commit では error のみブロック、CI では --strict で warning も含めて検知、という使い分けもできます。

既知の制限と使う上での考慮点

万能ではないので、現時点の制限も正直に書いておきます。ただ、どれも「知っていれば運用でカバーできる」類のものなので、制限ごとに「では利用者側はどう考慮すればいいか」までセットで整理します。

有効値リストは手動メンテ

src/agentlint/spec.py の有効値リスト(イベント名や model / effort / permissionMode の列挙値など)は、2026-07 時点の公式ドキュメント準拠です。Claude Code 側の仕様変更に自動追従はしないため、新しいイベント名やフロントマターのキーが追加されると、spec.py を更新するまでは誤検知(や見逃し)が起こり得ます。

使う側の考慮点としては、まず未知のキー(AL104)が warning 止まりなのは、まさにこの事態のための設計だと知っておくことです。仕様変更の直後に AL104 が出てもコミットはブロックされません。

公式ドキュメントに載っている正しいキーに対して AL104 が出ているなら、それは agentlint 側の追従漏れなので、その finding は無視して大丈夫です(そして spec.py に1行足せば直ります)。運用としては「公式ドキュメントの更新に気付いたら spec.py をメンテする」を回すイメージですね。

matcher の検証は Python の re による近似

hooks の matcher は Claude Code 内部では JavaScript の正規表現として解釈されますが、agentlint は Python の re モジュールで近似検証しています。両者の構文はおおむね互換とはいえ差異はあるので、JS では有効なのに Python では不正、といったパターンで誤検知 / 見逃しがあり得ます。

なので、AL303 の error が出たときは「即修正」ではなく、「実際に Claude Code 上でその hook が動くか」を先に確認するのがおすすめです。Claude Code 上で正しく動いているなら構文差異による誤検知の可能性が高いです。そのうえで、matcher をツール名の完全一致や Bash|Edit のような単純な alternation に寄せておくと、そもそもこの構文差異を踏まなくなります。

対象はプロジェクトスコープのみ

agentlint が見るのはプロジェクトスコープの .claude/settings.json / settings.local.json だけで、ユーザースコープの ~/.claude/ は対象外です。つまり、個人環境の ~/.claude/ に置いた設定が壊れていても検出されません。

これは「リポジトリにコミットされるものをコミット前に検証する」というツールの性格上の割り切りです。裏を返すと、チームで共有したい Skill / Agent / hooks はプロジェクトスコープ(リポジトリ内の .claude/)に寄せる運用が前提になります。

共有物をリポジトリ側に置いておけばすべて agentlint の検証対象に入りますし、個人設定の壊れは被害が本人で閉じるので、まずは共有物を守る、という優先順位です。

AL305 が見るのは「スクリプトパスと確信できるトークン」だけ

設計思想のところで書いた通り、AL305 の実在確認は .sh / .bash / .py / .js / .mjs / .ts の既知拡張子で終わるトークンだけが対象です。バイナリや拡張子なしスクリプトを直接実行している場合は、実在しなくても検出されません。また、引数位置のパスは先頭トークンがインタープリタ / ランナーの場合だけ見るので、find -exec 等の別コマンドに渡したスクリプトパスも見逃します。いずれも誤検知回避を優先した意図的な制限です。

裏を返せば、hooks の command を「インタープリタ + 拡張子付きスクリプトパス」の形(例: bash .claude/hooks/check.sh)に寄せておくと、AL305 の検証の恩恵をフルに受けられるということです。凝ったワンライナーを command に直書きするより、処理を .sh / .py に切り出してシンプルに呼ぶ——という、hooks の可読性の面でもどのみち好ましい書き方に倒すほど、Linter もよく効くようになります。

このほか細かい点として、.claude/commands/*.md では $CLAUDE_SKILL_DIR アンカーの参照を検証しません(commands では未定義のため)。

さいごに

今回は、Claude Code の .claude/ 配下を静的検証する自作 Linter「agentlint」を紹介しました。ポイントを整理します。

  • .claude/ 配下の設定ミスは実行するまで気付けず、Claude Code は壊れた設定を黙って無視することがある
  • agentlint はフロントマター検証・参照切れ検出・hooks 設定検証・再委譲禁止記述の検証の4チェックをコミット前に静的実行する
  • 設計原則は「error にするなら保守的に。迷ったら検出しない」。error は構文・構造エラーと AL305 に限定し、誤検知でツールが放置される事態を避ける
  • pre-commit と CI に組み込めば、壊れた設定がリポジトリに入る前に検知できる

Skill や Subagent が増えてくると、.claude/ 配下は立派な「コード」です。コードなら Linter があって然るべき、ということで作ってみましたが、導入してからはフロントマターの typo やリネーム漏れをコミット前に何度も拾ってくれています。

みなさんも .claude/ が育ってきたら、設定の静的検証を仕組み化してみてはいかがでしょうか。この記事がその参考になれば嬉しいです!

ご覧いただきありがとうございます! この投稿はお役に立ちましたか?

役に立った 役に立たなかった

0人がこの投稿は役に立ったと言っています。
エンジニア募集中!

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です