Claude Codeでpublintとnpm packを回し、公開前に中身を点検する
npm publishの前に、npm packで同梱ファイルを、publintでpackage.jsonの宣言との食い違いを調べます。公開コマンドはPreToolUse hookで止める構成まで書きます。
npm publishの前に見るものは2つあります。tarball(公開される圧縮ファイル)に何が入るか。package.jsonの宣言が、入ったものと合っているか。前者はnpm pack、後者はpublintの仕事です。
publintは、npmパッケージの設定ミスと環境間の互換性の問題を報告するlinterです。ViteやWebpack、Rollup、Node.jsなど、読み込む側の違いを前提に検査します。
Claude Codeには、この2つの点検を回して結果を読ませます。公開コマンドそのものは、PreToolUse hookで一度止めます。点検が通らなければ、npm publishは実行されません。
npm packとpublintは何を分担するか
2つのツールは、見ているものが違います。
| ツール | 見えるもの | 見えないもの |
|---|---|---|
npm pack --dry-run --json | 見えるもの公開物に入るファイルのパスとサイズ | 見えないものそのパスをpackage.jsonが正しく指しているか |
publint | 見えるものmainやexportsが指すファイルの存在と公開、条件の並び順 | 見えないもの秘密のファイルが入っていないか(下の実験で確認) |
どちらか片方では足りません。npm packの一覧は、exportsが指すファイルが一覧の外にあっても何も言いません。publintはその食い違いを拾いますが、.env.localが入っていても黙っていました。
npmのdry-runは、変更を加えずに「何をするか」だけを報告するフラグです。npm packでもnpm publishでも使えます。
空のディレクトリで出力を見る
最小のパッケージを作って実行した出力を載せます。環境はNode v20.18.0、npm 10.8.2、publint v0.3.25です。
exportsの並びと存在しないファイル
dist/index.jsだけを置き、package.jsonをわざと崩します。
{
"name": "demo-lib",
"version": "1.0.0",
"type": "module",
"main": "./dist/index.cjs",
"exports": {
".": {
"default": "./dist/index.js",
"types": "./dist/index.d.ts"
}
},
"files": ["dist"]
}npx publint@0.3.25Running publint v0.3.25 for demo-lib...
Packing files with `npm pack`...
Linting...
Errors:
1. pkg.exports["."].types should be the first in the object as conditions are order-sensitive so it can be resolved by TypeScript.
2. pkg.exports["."].default should be the last in the object so it doesn't take precedence over the keys following it.
3. pkg.exports["."].types is ./dist/index.d.ts but the file does not exist.
4. pkg.main is ./dist/index.cjs but the file does not exist.4件とも、publintのルール一覧にある項目です。1はEXPORTS_TYPES_SHOULD_BE_FIRST、2はEXPORTS_DEFAULT_SHOULD_BE_LAST、3と4はFILE_DOES_NOT_EXISTに当たります。条件の並びが問題になるのは、exportsの条件が順序に意味を持つからです。
この出力の終了コードは1でした。エラーが1件でもあれば、--strictなしでも失敗として終わります。
exportsが指すファイルが公開されない
次は、ファイルは手元にあるのに公開物に入らない場合です。filesにdistだけを書いたまま、exportsでsrc/a.tsを指します。
{
"name": "demo-lib",
"version": "1.0.0",
"type": "module",
"exports": { ".": "./dist/index.js", "./raw": "./src/a.ts" },
"files": ["dist"]
}Errors:
1. pkg.exports["./raw"] is ./src/a.ts but the file is not published. Is it specified in pkg.files?FILE_NOT_PUBLISHEDです。手元ではsrc/a.tsが存在するので、パスの打ち間違いを探しても見つかりません。ルール一覧には、このエラーはpublintをローカルで実行したときだけ出ると書かれています。
filesを書かないと何が入るか
filesを外し、exportsだけにします。.env.local、src/、test/を置いたままにします。
{
"name": "demo-lib",
"version": "1.0.0",
"type": "module",
"exports": "./dist/index.js"
}npm pack --dry-run --json | jq -r '.[0].files[].path'.env.local
dist/index.js
package.json
src/a.ts
test/a.test.jspublintの出力は、次のとおりです。
Suggestions:
1. The package publishes internal tests or config files (/test/). You can use pkg.files to only publish certain files and save user bandwidth.終了コードは0でした。指摘はtest/だけで、.env.localには触れていません。USE_FILESはテストと設定ファイルの混入を知らせる提案(suggestion)で、秘密のファイルを探す検査ではありません。公開物の中身を読む役は、npm packの一覧が担います。
filesで入るものを決める
同梱ファイルの決まり方は、npmのpackage.jsonのページに書かれています。
filesを省くと、既定の["*"]としてすべてのファイルが対象になる.npmignoreがなく.gitignoreがあれば、.gitignoreの内容が使われるpackage.json、READMEとLICENSE、mainのファイル、binのファイルは、設定にかかわらず入るnode_modules、.npmrc、package-lock.jsonなどは、既定で除外される
.env.localは、除外の一覧にありません。上の実験で入っていたのはこのためです。
安全側に倒すなら、filesで入れるものを列挙します。除外を書き足していく方式だと、新しい種類のファイルが増えるたびに穴が開きます。filesにdistだけを書いた最初の例では、npm packの一覧はdist/index.jsとpackage.jsonの2件でした。
Claude Codeに点検を回させる
点検の手順は、毎回同じ順です。順序をCLAUDE.mdに書いておくと、指示のたびに繰り返さずに済みます。
## 公開前の点検
- `npm publish`は自分で実行しない。必要なら止めて報告する
- 点検は`npm run build`、`npx publint --strict`、
`npm pack --dry-run --json`の順に実行する
- publintのErrorsは`package.json`を直し、同じコマンドで消えたことを確かめる
- `files[].path`を読み、`.env`系、テスト、ソースマップ、鍵ファイルが
入っていないか報告する手順としては、次の4段階です。
公開前の点検を回す順番
- 1
ビルドを先に済ませる
publintは公開されるファイルを検査します。ビルドが必要なパッケージは、publintの前にビルドを走らせます。
distが無い状態で実行すると、FILE_DOES_NOT_EXISTが並びます。 - 2
publintを厳しめに実行する
npx publint --strictは、警告もエラーとして扱います。Claude Codeには出力をそのまま読ませ、指摘ごとにpackage.jsonのどのキーかを答えさせます。 - 3
同梱ファイルの一覧を読む
npm pack --dry-run --jsonのfiles[].pathを一覧にさせ、想定外のファイルを挙げさせます。 - 4
tarballそのものを検査する
npm packで作ったtarballのパスを、publint ./demo-lib-1.0.0.tgzのように渡せます。公開直前のtarballを、そのままの形で検査したいときの手段です。
publintのCLIは、引数にディレクトリでもtarballでも受け取ります。オプションは--level(出力する深刻度)、--pack(packに使うパッケージマネージャー)、--strictの3つです。
Claude Codeへの指示は、公開しないことを明示するのが要点です。「公開できる状態か点検して」とだけ頼むと、点検と公開の境目があいまいになります。公開の実行は人が、点検の実行と結果の読み取りはClaude Codeが受け持つ分担にします。
PreToolUse hookで公開コマンドを止める
CLAUDE.mdの指示は、守られなかったときに止められません。止める役は、PreToolUse hookに任せます。このhookはツールの実行前に走り、終了コード2でその呼び出しをブロックします。
設定は.claude/settings.jsonに書きます。
{
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"if": "Bash(npm publish *)",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/precheck-publish.sh",
"args": [],
"timeout": 120,
"onFailure": "block"
}
]
}
]
}
}matcherでBashツールに絞り、ifでさらにnpm publishを含むコマンドに絞ります。Bash(npm publish *)は、末尾の*の前にスペースがあるため、引数のないnpm publishにも一致します。
スクリプトは、標準入力のJSONからコマンドを取り出します。点検に落ちたら標準エラーに理由を書き、終了コード2で終わります。
#!/bin/bash
# npm publish の前に、公開物の中身を点検する
input=$(cat)
command=$(jq -r '.tool_input.command' <<<"$input")
case "$command" in
*"npm publish"*) ;;
*) exit 0 ;;
esac
cd "${CLAUDE_PROJECT_DIR:-.}" || exit 2
# 1. publint: 警告もエラー扱いにする
if ! report=$(npx --yes publint@0.3.25 --strict 2>&1); then
echo "publint が問題を検出しました。直してから公開してください。" >&2
echo "$report" >&2
exit 2
fi
# 2. 同梱ファイルに、公開してはいけないものが無いか
leaked=$(npm pack --dry-run --json 2>/dev/null \
| jq -r '.[0].files[].path' \
| grep -E '(^|/)\.env|\.pem$|(^|/)test/' || true)
if [ -n "$leaked" ]; then
echo "公開物に含めたくないファイルが入っています:" >&2
echo "$leaked" >&2
exit 2
fi
exit 0chmod +x .claude/hooks/precheck-publish.shで実行権限を付けます。
スクリプト単体の動作
Claude Codeを介さず、標準入力にJSONを流して終了コードを見ました。先ほどの「filesを書かない」パッケージで、npm publishのJSONを渡すと次の出力でした。
公開物に含めたくないファイルが入っています:
.env.local
test/a.test.js終了コードは2です。lsを渡すと、何も出力せず終了コード0で通ります。
hookの標準エラーは、Claude Codeにツールのエラーとして返ります。PreToolUse:Bash hook errorにhookのコマンドが続き、そのあとにスクリプトのメッセージが付く形です。Claude Codeはこれを読み、package.jsonの修正に進めます。
止まらなくなる条件
落とし穴
スクリプトがexit 1で終わると、公開は止まらない
hookで「止める」意味を持つ終了コードは2です。スクリプトの途中でエラーになり、1などで終わると、非ブロッキングのエラーとして扱われ、npm publishは実行されます。パスの誤りでスクリプトが起動しない場合も同じです。
onFailureを"block"にすると、起動失敗や0・2以外の終了コード、タイムアウトでも呼び出しを止められます。このフィールドはClaude Code v2.1.295以降が対象です。
ほかにも、押さえておく点が3つあります。
timeoutの既定は、commandフックで600秒です。点検でnpxがネットワークを使うので、上限を短く明示しますifは最善努力のフィルターです。Claude Codeがコマンドの中身を判別できないときは、パターンに関係なくhookが走ります。スクリプト側でcaseを持つのは、そのためです- PreToolUseが走るのは、Claude Codeがツールを呼んだときだけです。自分でターミナルに打った
npm publishは止まりません
Claude Codeに公開を一切させないなら、hookではなく権限の拒否ルールを使います。hookは「点検が通れば公開してよい」という条件付きの関門に向きます。
PowerShellでhookを動かす環境では、exit 2で止まらない症状が報告されています。PowerShell hookがexit 2で止まらない問題に切り分けをまとめています。hookの基本的な書き方はClaude Code hooksガイドにあります。
点検で分かることと分からないこと
公開前の点検で拾えるのは、公開物の形に関する問題までです。公開の瞬間まで出ない問題は残ります。
| 確かめたいこと | 手段 | 公開前に分かるか |
|---|---|---|
| 同梱ファイルの過不足 | 手段npm pack --dry-run --json | 公開前に分かるか分かる |
main・exports・typesの食い違い | 手段publint --strict | 公開前に分かるか分かる |
| 依存パッケージの脆弱性 | 手段npm audit | 公開前に分かるか分かる(別の検査) |
| trusted publisherの登録内容 | 手段初回の公開 | 公開前に分かるか分からない |
| provenanceが付くか | 手段公開workflow | 公開前に分かるか分からない |
trusted publishingの登録は保存時に検証されず、誤りは公開を試みたときにしか出ません。dry-runで見えるのは、公開物の中身までです。この境目はnpm trusted publishingをClaude Codeで組むで扱っています。
依存の脆弱性は、publintの守備範囲の外です。Claude Codeでnpm auditとpip-auditの結果を仕分けるの手順で別に点検します。公開workflowの設定ミスを調べるなら、Claude CodeでGitHub Actionsの脆弱性を点検するも使えます。
つまずきやすい点
publintの動作条件は、ドキュメントに書かれています。
- ローカル実行にはNode.js 18以上が必要です。対応するのはnpm(v9〜v12)、yarn(v3、v4)、pnpm(v8〜v12)、bunです
- Yarn 1は対象外です
--strictは警告(warnings)をエラーとして報告するフラグで、既定はオフです--level warningを付けると、suggestionsを出力から外せます
npx --yes publint@0.3.25のようにバージョンを固定すると、hookが実行されるたびに版が変わることを避けられます。プロジェクトで継続して使うなら、devDependenciesにpublintを入れ、package.jsonに"lint:package": "publint"のようなスクリプトを置く方法もあります。その場合のhookはnpm run lint:packageを呼ぶだけで済みます。
まとめ
公開前の点検は、npm packで「何が入るか」を読み、publintで「宣言と合っているか」を照合する2段で成り立ちます。publintの指摘は.env.localのような秘密のファイルに及ばないため、同梱ファイルの一覧は別に読みます。
Claude Codeには点検を回して結果を読む役を渡し、npm publishの実行はhookで関門を作ります。hookの終了コードは0か2だけを使い、onFailure: "block"も併用すると、スクリプトの不具合で公開が通る事故を避けられます。
参照した一次ソース
この記事の記述は、次の一次ソースと照合しています。更新時に参照先が変わった場合は一覧も更新します。