Claude Media
Claude Codeでpublintとnpm packを回し、公開前に中身を点検する

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.25
Running 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.js

publintの出力は、次のとおりです。

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. 1

    ビルドを先に済ませる

    publintは公開されるファイルを検査します。ビルドが必要なパッケージは、publintの前にビルドを走らせます。distが無い状態で実行すると、FILE_DOES_NOT_EXISTが並びます。

  2. 2

    publintを厳しめに実行する

    npx publint --strictは、警告もエラーとして扱います。Claude Codeには出力をそのまま読ませ、指摘ごとにpackage.jsonのどのキーかを答えさせます。

  3. 3

    同梱ファイルの一覧を読む

    npm pack --dry-run --jsonのfiles[].pathを一覧にさせ、想定外のファイルを挙げさせます。

  4. 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 0

chmod +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"も併用すると、スクリプトの不具合で公開が通る事故を避けられます。

参照した一次ソース

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

  1. publint.dev/docs
  2. publint.dev/docs/cli
  3. publint.dev/rules
  4. docs.npmjs.com/cli/v11/commands/npm-pack
  5. docs.npmjs.com/cli/v11/configuring-npm/package-json
  6. code.claude.com/docs/en/hooks
この記事を共有:XはてブLinkedIn