ある朝、package.json を開いて手が止まりました。date-fns の隣に、入れた覚えのない小さな日付ライブラリが並んでいます。git blame をかけると、3週間前のエージェントのコミットでした。テストを通すために足したのでしょう。動いてはいます。けれど「なぜこれを選んだのか」「ライセンスは何か」「外しても平気か」は、もうどこにも書かれていませんでした。
エージェントにコードを任せると、生産性は確かに上がります。その裏で、依存ツリーは静かに、こちらの記憶より速く育ちます。一つひとつは小さな判断でも、数ヶ月・複数プロジェクトぶんが積み重なると、誰も全体像を把握していない状態になります。これは長期運用でいちばん効いてくる種類の負債です。
ここでは、エージェントが足した依存を後から棚卸しし、ライセンスと出所を追える形に戻すための設計を整理します。
追えなくなると、何が起きるのか
「見覚えのない依存が一つある」だけなら笑い話で済みます。問題は、それが積み上がったときに現れます。
一つ目は、ライセンスの取り違えです。エージェントは機能を満たすパッケージを選びますが、ライセンス条件まで毎回吟味してくれるとは限りません。GPL 系のコードが商用クローズドのアプリに紛れ込んでいても、棚卸しの仕組みがなければ気づけません。
二つ目は、攻撃面の拡大です。直接依存が一つ増えると、推移的依存は十、二十と連れてきます。使っていない依存が放置されるほど、脆弱性の通知に追われる時間が増えます。
三つ目は、判断の喪失です。半年後に「この依存、外していいんだっけ」と迷ったとき、足した理由が残っていなければ、外す勇気も残せません。結果として、誰も触れない依存が永久に居座ります。
私自身、Dolice Labs として複数のサイトとアプリを個人開発で並行して回しているので、この三つはどれも実感があります。とくに三つ目の「判断の喪失」は、エージェント運用に固有の重さがあると感じています。人が足した依存には、たいてい記憶か Slack のログが残ります。エージェントが足した依存には、コミットメッセージ以上の文脈が残らないことが多いのです。
棚卸しを「三つの問い」に分解する
棚卸しを漠然と始めると、依存の多さに圧倒されて手が止まります。問いを三つに分けると、機械にやらせる部分と人が見る部分の線引きが明確になります。
| 問い | 知りたいこと | 自動化の可否 |
| 何が | 直接依存のうち、エージェントが追加したものはどれか | ほぼ自動化できる |
| いつ | どのコミットで、どの作業の流れで入ったか | 自動化できる |
| なぜ | その依存でなければならない理由があったか | 人の確認が要る |
「何が」「いつ」は git 履歴に答えがあります。「なぜ」だけは、後から人が判断を補う必要があります。だからこそ、足した時点で「なぜ」を残しておく運用が効いてきます。後半でそこに触れます。
実装1:エージェントが足した依存を抽出する
まず「いつ・何が足されたか」を機械的に取り出します。package.json の dependencies ブロックに行が追加されたコミットを、git log -p で追いかける方法です。エージェントのコミットを author や committer のメールアドレスで識別できる運用にしておくと、絞り込みが正確になります。
次のスクリプトは、dependencies セクションに + で追加された行と、その導入コミットの日時・著者・件名を一覧にします。
#!/usr/bin/env bash
# scan-added-deps.sh — package.json の dependencies に追加された依存を履歴から抽出する
set -euo pipefail
PKG="package.json"
# dependencies / devDependencies に追加された行だけを、導入コミットの文脈つきで拾う
git log --diff-filter=AM -p --date=short \
--pretty=format:'@@COMMIT@@%h|%ad|%an|%s' -- "$PKG" \
| awk '
/^@@COMMIT@@/ {
sub(/^@@COMMIT@@/, "");
split($0, m, "|");
sha=m[1]; date=m[2]; author=m[3]; subject=m[4];
next;
}
# 追加行のうち " \"pkg\": \"^x.y.z\"," 形式だけを対象にする
/^\+[[:space:]]+"[^"]+":[[:space:]]*"[~^]?[0-9]/ {
line=$0;
sub(/^\+[[:space:]]+"/, "", line);
sub(/".*/, "", line);
printf "%-28s | %s | %-18s | %s\n", line, date, author, subject;
}
' | sort -u
実行すると、こうした行が並びます。
date-fns-tz | 2026-05-31 | antigravity-agent | fix: タイムゾーン変換のテストを通す
zod | 2026-06-04 | antigravity-agent | feat: 入力バリデーションを追加
p-retry | 2026-06-09 | masaki | chore: リトライ処理を共通化
著者の列を見れば、どれがエージェント由来かが一目で分かります。p-retry のように自分で足したものは記憶もあるはずです。問題は date-fns-tz のような、エージェントが静かに入れたものです。これが棚卸しの主役になります。
ポイントは、devDependencies も同じ仕組みで拾えることです。ビルドツールや型定義は数が多く、エージェントが増やしやすい領域なので、本番依存と分けて見ておくと負荷が下がります。
実装2:ライセンスを集計し、ポリシーと突き合わせる
「何が・いつ」が分かったら、次は「そのライセンスは許容できるか」です。ここは完全に自動化できます。インストール済みの依存からライセンスを収集し、自分のポリシーに照らして色分けします。
ライセンス情報は node_modules 内の各 package.json に書かれています。専用ツールを入れずに、標準の Node だけで集計できます。
// audit-licenses.mjs — インストール済み依存のライセンスをポリシーと突き合わせる
import { readFile, readdir } from "node:fs/promises";
import { join } from "node:path";
// 自分の運用ポリシー。プロジェクトの性質に合わせて調整する
const POLICY = {
allow: ["MIT", "ISC", "Apache-2.0", "BSD-2-Clause", "BSD-3-Clause", "0BSD"],
review: ["MPL-2.0", "LGPL-3.0", "CC0-1.0", "Unlicense"],
// それ以外(GPL 系・ライセンス不明)は deny 扱いにする
};
async function collect(dir) {
const out = [];
for (const name of await readdir(dir)) {
if (name.startsWith(".")) continue;
const base = join(dir, name);
if (name.startsWith("@")) {
for (const scoped of await readdir(base)) {
out.push(...(await readOne(join(base, scoped), `${name}/${scoped}`)));
}
} else {
out.push(...(await readOne(base, name)));
}
}
return out;
}
async function readOne(base, name) {
try {
const pkg = JSON.parse(await readFile(join(base, "package.json"), "utf8"));
const license = typeof pkg.license === "string" ? pkg.license : "UNKNOWN";
return [{ name, version: pkg.version ?? "?", license }];
} catch {
return [];
}
}
function classify(license) {
if (POLICY.allow.includes(license)) return "allow";
if (POLICY.review.includes(license)) return "review";
return "deny";
}
const deps = await collect("node_modules");
const flagged = deps
.map((d) => ({ ...d, tier: classify(d.license) }))
.filter((d) => d.tier !== "allow")
.sort((a, b) => a.tier.localeCompare(b.tier));
for (const d of flagged) {
console.log(`[${d.tier.toUpperCase()}] ${d.name}@${d.version} — ${d.license}`);
}
const denied = flagged.filter((d) => d.tier === "deny");
if (denied.length > 0) {
console.error(`\n⚠️ 要対応: ${denied.length} 件が deny ポリシーに該当します`);
process.exit(1);
}
allow に入るものは出力しません。人が見るべきは review と deny だけだからです。CI に組み込めば、deny が混ざった時点でビルドを止められます。エージェントが新しい依存を足したプルリクエストで、このチェックが落ちる——それが棚卸しを「定期作業」から「混入を未然に止める仕組み」へ変える瞬間です。
ライセンスの表記はパッケージによって揺れます。license が文字列ではなくオブジェクトだったり、licenses 配列の古い形式だったりします。最初は UNKNOWN が大量に出るはずなので、件数の多いものから手当てするのが現実的です。完璧な分類より、deny を取りこぼさないことを優先します。ここは見落としやすい注意点で、UNKNOWN をそのまま放置すると、本番運用に入ってから慌てて確認する羽目になります。早めに手当てして回避しておくのが安全です。
許容方針は、私の場合おおむね次の三段で運用しています。
| 区分 | 方針 | 具体例 |
| allow | 確認なしで使う | MIT, ISC, Apache-2.0, BSD 系 |
| review | 用途を確認してから使う | MPL-2.0, LGPL 系 |
| deny | 原則使わない・代替を探す | GPL 系, ライセンス不明 |
この区分はプロジェクトの性質で変わります。クローズドな商用アプリと、公開予定の OSS では deny の線引きが違って当然です。大切なのは、どこかに明文化して、エージェントにも人にも同じ基準を適用することです。クローズドな商用アプリでは、review 区分も保守的に倒すことを推奨します。
「なぜ」を、足した瞬間に残す
「何が・いつ・ライセンスは何か」までは機械化できました。残るのは「なぜ」です。これだけは後から自動では復元できません。だからこそ、足す瞬間に薄くでも残す仕組みが要ります。
私が落ち着いたのは、コミットの末尾に出所トレーラーを付ける運用です。Git のトレーラーは key: value 形式の追記で、後から git log で機械的に拾えます。
feat: 入力バリデーションを zod で追加
スキーマ定義を一箇所に集約し、API 境界で検証する。
Dep-Added: zod@^3.23
Dep-Reason: 手書きの型ガードが増えすぎたため、宣言的に置き換える
Dep-Reviewed-By: masaki
エージェントへの指示(AGENTS.md などのルールファイル)に「依存を追加したら Dep-Added と Dep-Reason のトレーラーを必ず書く」と一行加えておくと、出所が記録として残り始めます。完璧を狙う必要はありません。一行の理由があるだけで、半年後の棚卸しの体感は大きく変わります。
トレーラーを集計したいときは、こう拾えます。
git log --pretty='%H %s%n%b' \
| grep -E '^Dep-(Added|Reason|Reviewed-By):' \
| sort | uniq -c | sort -rn
長期運用での線引き
ここまでの仕組みを毎日手で回す必要はありません。むしろ、回し方を決めておくことが設計の本体です。私は次のように分けています。
混入を止める部分は CI に常駐させます。ライセンスチェックはプルリクエストごとに走らせ、deny で必ず落とす。ここは人の意志を挟まず、機械に任せきります。
棚卸しそのものは、月に一度で十分です。私の場合、次の三手順で回しています。
scan-added-deps.sh を流し、その月にエージェントが足した依存を一覧にします。
Dep-Reason の無いものだけ、足した理由を一行だけ補います。
- どこからも参照されていない依存を外します。
慣れれば三十分もあれば終わります。
そして、判断が必要な review だけは、その都度ちゃんと自分で見ます。ここを自動化しようとすると、結局ポリシーが形骸化します。人が見る範囲を狭く保つことが、長く続けるコツだと考えています。
エージェントは、こちらが基準を明示すればその範囲では驚くほど律儀に働きます。問題は、基準を渡さないまま「うまくやっておいて」と任せ続けることです。依存の棚卸しは、その基準を言葉にして、機械と人で分担する練習でもあります。
次の一歩として、まずは scan-added-deps.sh を自分のリポジトリで一度流してみてください。直近一ヶ月で、記憶にない依存がいくつ出てくるか——その数が、棚卸しを仕組みにする価値の手応えになるはずです。同じように複数プロジェクトを抱えている方の参考になれば幸いです。