はじめに
CLAUDE.md や AGENTS.md は、書いた直後がいちばん正確です。ディレクトリを一つリネームした瞬間から、誰も気づかないまま少しずつ嘘になります。エージェントはその嘘を疑わずに読み、存在しないパスを探し、無駄なトークンを使います。
対象読者は、CLAUDE.md / AGENTS.md をチームで運用していて、その鮮度を機械的に検査したいと考えている開発者です。
この記事では、agents-lint という零依存の CLI を、記事執筆を自動化している本ブログのリポジトリにかけた結果を扱います。初回スコアは 0/100(F 判定) でした。ただしエラー 7 件のうち 6 件は誤検出で、設定を 6 行足したら 56/100 に戻ります。そして、その回復のさせ方を一歩間違えると、CI は緑のまま中身が壊れた状態になります。
agents-lint が検査するもの
agents-lint は、AGENTS.md / CLAUDE.md / GEMINI.md / MEMORY.md といったエージェント向けコンテキストファイルを静的に検査し、リポジトリの実態とのズレ(context rot)を報告する CLI です。
パッケージの実測値は次のとおりです。npm view agents-lint と、npm pack で取得した tarball を展開して確認しました。
| 項目 | 実測値 |
|---|---|
| 最新バージョン | 0.5.0 |
| dependencies |
{}(零依存) |
| engines.node | >=18.0.0 |
| ライセンス | MIT |
| 配布物サイズ | 372 KB(展開後) |
零依存は README の主張ではなく、package.json の dependencies が空オブジェクトであることを確認した結果です。npx agents-lint で即実行できるのはこの構成のおかげです。
検査は 5 つのチェッカーに分かれています。
| チェッカー | 見るもの |
|---|---|
| structure | 必須セクション(Setup / Testing / Build)の有無、ファイル長 |
| filesystem | 言及されたパスが実在するか |
| npm-scripts | 言及されたスクリプトが package.json にあるか |
| dependencies | 言及されたパッケージが package.json にあるか |
| framework-staleness | フレームワーク固有の古い記法 |
実運用リポジトリにかけたら 0 点でした
検査対象の CLAUDE.md は 52,653 バイトあり、スキル一覧・スクリプト一覧・運用ルールへの参照が詰まっています。
npx agents-lint@0.5.0 CLAUDE.md --no-color
出力の冒頭がこれでした。
Freshness Score
░░░░░░░░░░░░░░░░░░░░ 0/100 (F)
内訳は 7 errors / 3 warnings / 1 info です。0 点と言われると身構えますが、エラーの中身を一件ずつ見ると印象が変わります。
✖ Path does not exist: "/next":37
- **コマンド**: `/next`(次タスク自律判定)・`/status`(現状把握)。
✖ Path does not exist: "/status":37
✖ Path does not exist: "/collect-news":217
✖ Path does not exist: "/weekly-retrospective":223
✖ Path does not exist: "/skill-inventory-audit":224
✖ Path does not exist: "/governance-review":225
✖ Path does not exist: "/images/":74
7 件のうち 6 件は スラッシュコマンド です。/next や /collect-news は Claude Code のカスタムコマンド名であって、ファイルシステムのパスではありません。filesystem チェッカーはバッククォートで囲まれていてもスラッシュ始まりの表記をパス候補として拾い、fs.existsSync にかけて、無いと報告します。
同じ勘違いは警告側にも波及していました。
⚠ Package "next" is mentioned but not found in package.json:37
⚠ Directory "/lib" is referenced but does not exist in the repo root:187
前者は /next というコマンド名から Next.js の存在を推定したもので、このリポジトリは Zenn 記事と Node スクリプトの集合体です。後者は scripts/lib/x-post-builder.js という記述からパスの一部を切り出し、リポジトリ直下のディレクトリとして解釈しています。scripts/lib/ は実在します。
最小再現で挙動を確定させる
本物のリポジトリは変数が多すぎるので、空のディレクトリで切り分けました。package.json と src/index.js を置き、AGENTS.md には 2 行だけ書きます。片方は実在するファイル、もう片方はスラッシュコマンドです。
- `/next` runs the next-task command.
- See `src/index.js` for the entry point.
npx agents-lint@0.5.0 --format json
JSON 出力の該当部分がこれです。
{
"checker": "filesystem",
"issues": [
{
"rule": "no-missing-path",
"severity": "error",
"message": "Path does not exist: \"/next\"",
"line": 5,
"context": "- `/next` runs the next-task command."
}
],
"passed": 1,
"failed": 1
}
実在する src/index.js は passed に入り、/next だけがエラーになりました。バッククォートで囲まれていても、コード記法かどうかは判定に影響しません。
先頭のスラッシュを外して `next` (slash command) と書き直すと、エラーは 0 件になりました。原因はスラッシュで始まる表記そのものです。
回避策1: ignorePatterns で除外する
リポジトリ直下に .agents-lint.json を置くと設定を上書きできます。--help の出力には現れず、README の Custom rules 節にだけ記載があるオプションです。配布物の dist/config.js を読むと、探しに行くファイル名は次の 2 つでした。
const CONFIG_FILES = ['.agents-lint.json', '.agents-lint.config.json'];
除外の判定ロジックは同じく dist/checkers/filesystem.js にあります。
function isIgnored(value, patterns) {
return patterns.some((p) => value.includes(p));
}
部分一致です。抽出された文字列に指定パターンが含まれていればスキップされます。そこで、コマンド名をそのまま列挙しました。
{
"ignorePatterns": [
"/next",
"/status",
"/collect-news",
"/weekly-retrospective",
"/skill-inventory-audit",
"/governance-review"
]
}
この 6 行を置いて再実行した結果です。
| 設定前 | 設定後 | |
|---|---|---|
| Freshness Score | 0/100 | 56/100 |
| errors | 7 | 1 |
| warnings | 3 | 3 |
残った 1 件のエラーは /images/ でした。これは誤検出ではありません。記事画像を Cloudflare R2 で配信していて images/ を gitignore しているため、クリーンな clone には実体がないのです。ツールの言い分のほうが正しく、CLAUDE.md 側の記述が実態とズレていました。0 点の山を崩したら、本当に直すべき 1 件が下から出てきた形です。
なお ignorePatterns は filesystem / dependencies / cross の各チェッカーが個別に参照します。/next を除外しても、dependencies チェッカーが出す Package "next" is mentioned but not found は消えませんでした。抽出のしかたが違うためで、パッケージ名側の除外は別途必要です。
回避策2: severity を落とすと CI が静かに緑になる
.agents-lint.json は severity も上書きできます。誤検出が多いなら、エラーを警告に落としてしまえばいいように見えます。
{
"severity": { "missingPath": "warn" }
}
これで再実行すると、こうなりました。
score 12 errors 0 warnings 10
そして終了コードを取ると、
npx agents-lint@0.5.0 CLAUDE.md > /dev/null 2>&1; echo "exit=$?"
# exit=0
12 点のまま、CI は通ります。 終了コードの決定は dist/cli.js の次の分岐です。
if (report.errors > 0)
process.exit(1);
if (maxWarnings !== undefined && report.warnings > maxWarnings) {
process.exit(1);
}
process.exit(0);
--max-warnings を渡さない限り、警告は何件あっても終了コードに影響しません。severity を落とす回避策は、誤検出を黙らせると同時に、本物の劣化も黙らせます。
実測した終了コードの組み合わせを表にします。検査対象は errors 0 / warnings 5 の状態です。
| 実行 | 終了コード |
|---|---|
agents-lint AGENTS.md |
0 |
agents-lint AGENTS.md --max-warnings 0 |
1 |
agents-lint(ファイル引数なし) |
0 |
errors が 1 件以上ある状態(--max-warnings の有無を問わず) |
1 |
CI に入れるなら --max-warnings の指定が必須です。README にも npx agents-lint --max-warnings 5 の例が載っていますが、この引数を落とすと警告を見張る意味がなくなる点は明示されていません。
JSON 出力の形が引数で変わる
機械処理する側には、もう一つ面倒な仕様があります。ファイル引数の有無で JSON のトップレベル構造が変わります。
# ファイル引数あり
# → { "file": "...", "score": 0, "results": [...], "errors": 7, "warnings": 3 }
# ファイル引数なし(auto-detect)
# → { "files": ["AGENTS.md"], "reports": [ { "file": ..., "score": ..., "results": [...] } ] }
自動検出の経路では reports 配列に入り、単一ファイル指定では報告オブジェクトが直接トップレベルに来ます。筆者は最初 j.reports[0] でパースするスクリプトを書き、Cannot read properties of undefined で落としました。集計するなら j.reports ? j.reports[0] : j で両方を吸収しておくのが安全です。
誤検出を除いたあとに残った、本当の指摘
ノイズを落としたあとに残ったのは 3 件で、どれも妥当な指摘でした。
1. ファイルが長すぎる(info)
ℹ Context file is very long (> 15,000 characters) — context bloat increases cost by 20%+
このリポジトリの CLAUDE.md は 52,653 バイトで、閾値の 3 倍以上あります。エージェントが毎セッション読む常駐ファイルなので、素直に効いてくる指摘です。
2. test スクリプトがない(warn)
package.json に test がなく、検証系は npm run lint:md や --self-test に分散していました。エージェントが「変更したら何を叩けば検証できるか」を一発で見つけられない構成である、という指摘は当たっています。
3. /images/ が実在しない(error)
R2 配信への移行後、CLAUDE.md の記述が追いついていませんでした。
全 11 件(errors 7 / warnings 3 / info 1)の指摘のうち、行動に値したのは 3 件です。比率としては褒められたものではありませんが、このツールがなければ 3 件とも気づかないままでした。
CI に入れるなら
以上を踏まえると、誤検出は ignorePatterns で落とし、severity はエラーのまま維持し、--max-warnings で閾値を明示する形になります。
name: agents-lint
on:
push:
paths: ['CLAUDE.md', 'AGENTS.md', 'package.json']
schedule:
- cron: '0 0 * * 1'
jobs:
lint:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: npx agents-lint@0.5.0 CLAUDE.md --max-warnings 3
バージョンを固定するのは、検査ルールが増えると閾値の意味が変わるためです。スケジュール実行を入れておくと、コンテキストファイルを触っていない週でも、まわりのコードが動いたことによる劣化を拾えます。
著者視点の発見ポイント
筆者がこのツールを試して意外だったのは、スコアの絶対値がほとんど役に立たなかった ことです。0/100 という数字は「このリポジトリの CLAUDE.md は壊れている」と言っているように見えますが、内訳を開くまで意味がありませんでした。逆に、severity を触るだけでスコアは 0 から 12 に、ignorePatterns を足せば 56 まで動きます。同じファイルのまま設定だけで 3 倍以上変わる指標を、チームの KPI に据えるのは危ういと感じました。
有用だったのは、スコアではなく --format json の results 配列です。チェッカー単位・ルール単位で件数を数えると、ノイズの塊がすぐ見えます。筆者は次のスクリプトでルール別の件数を出しました。
npx agents-lint@0.5.0 CLAUDE.md --format json > report.json
node -e '
const j = require("./report.json");
const r = j.reports ? j.reports[0] : j;
console.log("score", r.score);
for (const c of r.results) {
const m = {};
for (const i of c.issues) {
const k = i.rule + "/" + i.severity;
m[k] = (m[k] || 0) + 1;
}
console.log(c.checker, JSON.stringify(m));
}'
出力はこうなります。
score 0
structure {"too-long/info":1}
filesystem {"no-missing-path/error":7,"no-missing-directory/warn":1}
npm-scripts {"missing-test-script/warn":1}
dependencies {"no-missing-dependency/warn":1}
framework-staleness {}
この形にすると、エラー 7 件が一つのルールに固まっていることが即座に分かります。1 件ずつ本文を睨むより早く、除外すべきかコンテキストファイルを直すべきかの判断がつきました。
もう一点、この誤検出は「カスタムコマンドを CLAUDE.md に列挙する」運用が広がってから生まれた問題です。/deploy や /review を表に並べている CLAUDE.md は珍しくないはずで、同じ構成なら同じだけエラーが出ます。抽出ルールが追いつくまでは、ignorePatterns に自分のコマンド名を並べるのが現実的な対処になります。
まとめると
agents-lint は零依存で npx から即動き、コンテキストファイルの劣化を機械で拾う発想自体は正しいと感じました。ただし v0.5.0 時点ではスラッシュコマンドをパスとして解釈する誤検出があり、スコアをそのまま信じると判断を誤ります。
実運用に入れるときの要点は 3 つです。初回は --format json で内訳を出してルール単位の件数から誤検出の塊を特定すること。除外は severity を落とすのではなく ignorePatterns で行うこと(severity を落とすと 12 点のまま CI が緑になります)。そして CI では --max-warnings を必ず指定すること。
検証は Node v22.22.2・agents-lint 0.5.0 で実施しました。誤検出の挙動はバージョンで変わる可能性があるため、導入時はご自身のリポジトリで内訳を確認してから閾値を決めてください。
関連記事
- Agent設定をnpmのように配布するAPM入門 — 実機で見えた2つの罠
- claude-task-worker入門 — Issue/PRポーリングCLIを動かす
- Claude Codeの/rewind、事故った3時間を1コマンドで巻き戻せる仕組みだった