Claude Media
dependency-cruiserの循環依存と層またぎ違反をClaude Codeに直させる

dependency-cruiserの循環依存と層またぎ違反をClaude Codeに直させる

dependency-cruiserのforbiddenルールで循環依存と層の向きを機械判定し、違反一覧をClaude Codeに渡して直させる。PostToolUse hookで編集のたびに再検査する設定までを書きます。

循環依存と層またぎのimportは、テストが通っていても気づかないうちに増えます。Claude Codeが機能を足すとき、手近な関数を呼ぶために下の層から上の層へimportを張ってしまう、という形で入り込みやすい違反です。

dependency-cruiserは、依存関係を自分のルールで検証し、違反を報告するツールです。ルールを .dependency-cruiser.js に書き、違反の一覧をClaude Codeに渡し、PostToolUse hookで編集のたびに再検査します。この3点がそろうと、「依存の向きを直す」作業が目視ではなく機械判定の往復になります。

dependency-cruiserは何を検出するか

dependency-cruiserはJavaScript・TypeScriptなどのプロジェクトの依存を走査し、自分で書いたルールに照らして違反を報告するツールです。報告はtext(ビルド向け)とgraphics(人が眺める用)の2系統があります。

ここで使うのは前者です。forbidden セクションに「あってはならない依存」を並べ、severity を error にすると、違反が1件でもあれば非ゼロの終了コードで落ちます。READMEによると、終了コードは error 水準の違反の件数です。linterやtestと同じ扱いで、hookから結果を判定できます。

npx dependency-cruiser --init を実行すると、環境を見て質問に答える形で .dependency-cruiser.js が作られます。初期ルールには、循環依存の検出(no-circular)、package.jsonに無い依存、orphan(どこからも参照されないモジュール)、本番コードから開発用依存への参照といった、多くのプロジェクトで意味のあるものが入ります。

npm install --save-dev dependency-cruiser
npx dependency-cruiser --init

層の向きをforbiddenルールに書く

例として、src 配下が次の4層に分かれているプロジェクトを考えます。

  • src/ui — 画面とルーティング
  • src/service — ユースケース
  • src/domain — 業務ルールと型
  • src/infra — DBや外部APIのクライアント

依存の向きは ui → service → domain と infra → domain の一方通行にします。逆向きと、ui から infra への直接参照を禁止すると、次のようになります。

// .dependency-cruiser.js(forbidden の抜粋)
module.exports = {
  forbidden: [
    {
      name: "no-circular",
      severity: "error",
      comment:
        "循環依存。共有している型や定数を domain 側の別ファイルへ移し、" +
        "双方がそこを参照する形に直す。import 文だけを動かして隠さない",
      from: { path: "^src" },
      to: { circular: true },
    },
    {
      name: "domain-not-to-outer",
      severity: "error",
      comment:
        "domain は他の層を知らない。必要な振る舞いは domain 側にインターフェースを" +
        "置き、service か infra が実装する",
      from: { path: "^src/domain" },
      to: { path: "^src/(service|ui|infra)" },
    },
    {
      name: "service-not-to-ui",
      severity: "error",
      comment: "service から ui への参照は逆向き。ui が service を呼ぶ形に直す",
      from: { path: "^src/service" },
      to: { path: "^src/ui" },
    },
    {
      name: "ui-not-to-infra",
      severity: "error",
      comment: "ui は infra を直接呼ばず、service 経由にする",
      from: { path: "^src/ui" },
      to: { path: "^src/infra" },
    },
  ],
};

循環依存のルールは、READMEの例と同じ to: { circular: true } の形です。層のルールは from と to に正規表現で path を書くだけで、グロブではなく正規表現である点だけ注意が要ります。

commentに「どう直すか」まで書く

このルール定義で効くのが comment です。err-long という報告形式は、違反ごとに、そのルールの comment を併記します。公式の説明では、コメントに「なぜそのルールがあるか」と「どう直すか」を書いておくと、違反を文脈の中で読めます。

人間のレビュアーにも有用ですが、Claude Codeにとっては特に重みがあります。違反の一覧を受け取ったClaudeは、そこに書かれた直し方の方針をそのまま手がかりにできるからです。

no-circular のcommentに「import文だけを動かして隠さない」と入れているのには理由があります。循環を断つには、共有している型や定数を、双方が参照できる下の層へ移す必要があります。直し方の方針をルール側に持たせておくと、指示を毎回プロンプトで繰り返さずに済みます。

違反一覧を渡して依存の向きを直させる

まず手元で違反を出します。err-long は違反ごとにcommentを添える報告形式です。

npx dependency-cruiser src --output-type err-long

出力はルール名・from・toを並べたeslint風の一覧になります。これをClaude Codeに渡すときは、コマンドを実行させて結果を読ませるのが最短です。

npx dependency-cruiser src --output-type err-long を実行して、
error の違反をすべて直してください。
 
- 直したら同じコマンドを再実行し、違反が0件になるまで繰り返す
- 各ルールの comment に書かれた方針に従う
- 型の移動で解決できるものは移動先のファイルを先に作る
- 検査ルール(.dependency-cruiser.js)は編集しない

最後の一行が大事です。違反が消えない状況では、ルールを緩める方向に手が伸びるのを避けたいからです。.dependency-cruiser.js を編集させない運用にするなら、PreToolUse hookで該当ファイルへのEditを止める手もあります(公式hooksガイドのprotect-filesの例と同じ形です)。

手順

違反を直すときの往復

  1. 1

    検査を実行する

    err-long で違反とルールのcommentを取り出します。

  2. 2

    依存の向きを直す

    Claude Codeが共有の型や定数を下の層へ移し、importを付け替えます。

  3. 3

    同じコマンドで再検査する

    違反が0件になれば終了です。残っていれば2へ戻ります。

PostToolUse hookで編集のたびに再検査する

往復を手動の指示に頼ると、検査を回し忘れる回が出ます。PostToolUse hookにしておけば、ファイルを編集するたびに検査が走ります。

PostToolUseはツールの実行が成功した直後に発火するhookです。Edit|Write のようなマッチャーで、ファイル編集のあとだけに絞れます。ポイントは終了コードです。公式ドキュメントでは、PostToolUseのhookはexit 2で終了すると、stderrがClaudeに表示されます。ツールはすでに実行済みなので取り消しにはなりませんが、違反の一覧を次の判断の材料として渡せます。exit 0で終わったhookのstderrはClaudeには見えません。

.claude/hooks/check-deps.sh を作ります。

#!/bin/bash
INPUT=$(cat)
FILE=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
CWD=$(echo "$INPUT" | jq -r '.cwd')
 
# JS/TS 以外の編集では何もしない
case "$FILE" in
  *.ts|*.tsx|*.js|*.jsx) ;;
  *) exit 0 ;;
esac
 
cd "$CWD" || exit 0
OUT=$(npx dependency-cruiser src --output-type err-long 2>&1)
if [ $? -ne 0 ]; then
  echo "$OUT" >&2
  exit 2
fi
exit 0

実行権限を付け、.claude/settings.json に登録します。

chmod +x .claude/hooks/check-deps.sh
{
  "hooks": {
    "PostToolUse": [
      {
        "matcher": "Edit|Write",
        "hooks": [
          {
            "type": "command",
            "command": "\"$CLAUDE_PROJECT_DIR\"/.claude/hooks/check-deps.sh"
          }
        ]
      }
    ]
  }
}

スクリプトのパスに $CLAUDE_PROJECT_DIR を使うのは、公式hooksガイドの例と同じ書き方です。cwdが変わっても見つかるようにするためです。登録後は /hooks を実行して、PostToolUseの下に表示されることを確かめます。表示されなければ、JSONの末尾カンマやコメントの混入を疑います。

試すには、domain 配下のファイルに src/service をimportする1行をClaudeに足させます。その編集の直後にhookが走り、domain-not-to-outer の違反がcommentつきでClaudeに返れば成功です。hookをスクリプト単体で試す場合は、echo '{"tool_input":{"file_path":"src/domain/a.ts"},"cwd":"."}' | .claude/hooks/check-deps.sh; echo $? のようにサンプルJSONを流し、終了コードを確認できます。

落とし穴: 途中の状態で止められる、Bash経由の編集は見えない

便利な反面、この構成には外れどころがあります。

複数ファイルにまたがる直しの途中で違反が出る

循環を断つ直しは、型の移動先を作り、Aを直し、Bを直す、と複数ファイルを順に編集します。2つ目のファイルを直すまでの間は、一時的に別の違反が出る場合があります。編集ごとにhookが違反を返すので、Claudeはそのたびに応答しようとして、作業が細切れになります。

編集の途中を見逃したいなら、検査のタイミングをStop hookに移す選択肢があります。Stop hookはClaudeが応答を終えようとしたときに発火し、exit 2で停止を止めて作業を続けさせられます。ただし公式ドキュメントによると、Stop hookは8回連続でブロックすると、Claude Code側が上書きして止めます。スクリプトは入力の stop_hook_active が true かを見て、すでに継続中なら早めにexit 0する書き方が公式に示されています。

Stop hookのぶんだけ、違反の検出は遅くなります。向く場面は分かれます。

検査のタイミング向く場面注意
PostToolUse(編集ごと)向く場面1〜2ファイルの小さな修正。違反の混入をその場で知りたい注意複数ファイルの移動作業では途中の状態を拾う
Stop(応答の終わり)向く場面循環を断つ大きめのリファクタ注意8回連続ブロックで上書きされる。stop_hook_active の判定が必要

Bash経由のファイル変更はPostToolUseの対象外

公式ドキュメントには、Edit|Write のマッチャーのPostToolUse hookは、BashコマンドやClaude Codeの外のプロセスが同じファイルを書き換えたときには動かない、とあります。git mv やスクリプトによる一括置換でimportを書き換える作業は、このhookでは見えません。代わりの手段は2つ挙げられています。1つは、PostToolUse hookのマッチャーを省略するか "*" にして、hookの中で git status --porcelain を実行し、変わったファイルを自分で見つける方法です。もう1つは、ディスク上のファイルが変わったことを契機に動くFileChanged hookを使う方法です。

既存の違反が多すぎて、毎回止まる

導入時点ですでに循環が何十件もあるプロジェクトでerror水準にすると、hookは最初の編集から毎回失敗します。dependency-cruiserには既知の違反を記録して無視する仕組みがあります。

npx dependency-cruiser src --baseline

これで .dependency-cruiser-known-violations.json に現在の違反が保存されます。以降は --ignore-known を付けた実行が、記録済みの違反を無視し、新しい違反だけを出します。ドキュメントでは、--baseline は18.3.0以降の推奨の作り方とされています。hookのコマンドは --ignore-known を付けた形に変えます。

既存の違反を直す作業は別の段取りにします。baselineに残った違反をClaudeに1件ずつ直させ、直るたびに --baseline を更新する、という進め方です。--baseline-mode shrink-only を指定すると、既存の違反を消すだけで、新しい違反は書き込まれません。「基準線を悪化させる方向には更新しない」運用が機械的に守れます。

隣接する記事との使い分け

テストがない古いコードを安全に動かす進め方はClaude Codeリファクタリングを安全に進める手順にまとめています。そちらは「壊さずに動かす」ことが主題で、依存の向きを機械判定する手段は入っていません。この記事の設定は、その手順に足せる検査として使えます。

未使用のexportや依存を機械的に削る手順はClaude Codeでknipを使うが扱っています。knipが「使われていないもの」を探すのに対し、dependency-cruiserは「あってはならない依存」を探します。着眼点が違うので、併用できます。

hookのイベントや終了コードの全体像はClaude Code Hooks完全ガイドに、PostToolUseに絞った設定はPostToolUse hookとはにあります。Lintの自動修正に同じ型を使う場合はPostToolUse hookのLint自動修正が参考になります。

まとめ

層の向きと循環依存は、レビューで気づく問題ではなく、ルールで判定する問題に寄せたほうが、Claude Codeとの相性がよい領域です。forbidden ルールに comment で直し方まで書き、err-long で一覧を取り、PostToolUseかStopのhookで再検査します。

最初に決めるのは検査のタイミングです。小さな修正が多い現場なら編集ごとのPostToolUseで足ります。循環を断つ大きな直しを頼む日だけ、Stop hookに切り替える使い分けもあります。既存の違反が多いときは、先にbaselineを作ってからhookを入れます。

参照した一次ソース

この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。

  1. code.claude.com/docs/en/hooks-guide
  2. code.claude.com/docs/en/hooks
  3. github.com/sverweij/dependency-cruiser
この記事を共有:XはてブLinkedIn