ClawHubからのスコープ付きnpmパッケージがENOENTでインストールに失敗する
npmスタイルのスコープ付き名前(@scope/package)で公開されているプラグインとスキルは、一時アーカイブパスに解決されていないディレクトリセグメントが含まれているため、インストール中にENOENTエラーが発生します。
🔍 症状
主なエラー現象
ClawHubからスコープ付きnpmパッケージをインストールしようとすると、アーカイブ書き込みフェーズ中にENOENTエラーが発生します:
$ openclaw plugins install @axonflow/[email protected]
Resolving clawhub:@axonflow/[email protected]…
ClawHub code-plugin @axonflow/[email protected] channel=community verification=source-linked
Compatibility: pluginApi=>=2026.3.22 minGateway=2026.3.22
ClawHub package "@axonflow/openclaw" is community; review source and verification before enabling.
ENOENT: no such file or directory, open '/var/folders/ld/8b9xk7n52sg7q5vz7q1l8r840000gn/T/openclaw-clawhub-package-XXXXXX/@axonflow/openclaw.zip'
診断の観察結果
- スコープなしパッケージは成功: スコープセグメントのないパッケージはエラーなくインストールされます
$ openclaw plugins install mywallet # Output: Successfully installed plugin "mywallet"- エラーはバージョンに関係なく発生: すべての`@scope/name`パターンが、実際のスコープやパッケージ名に関係なく失敗をトリガーします
- 終了コード: プロセスは`ENOENT`(数値相当: `34`)で終了します
エラーオブジェクトの詳細
JavaScriptエラーオブジェクトは次のプロパティを提供します:
{
errno: -2,
code: 'ENOENT',
syscall: 'open',
path: '/var/folders/.../openclaw-clawhub-package-XXXXXX/@axonflow/openclaw.zip'
}重要な観察として、パスにはファイル名セグメントにスラッシュが含まれており、POSIXパス解決ではディレクトリ区切り文字として解釈されます。
🧠 原因
アーキテクチャ上の失敗シーケンス
dist/clawhub-CFvPS51z.jsのインストールパイプラインは次のシーケンスに従います:
- `fs.mkdtemp()`を使用して一意の一時ディレクトリを作成
- 一時ディレクトリとパッケージ名を結合してアーカイブパスを構築
- ダウンロードしたzipバイトを一括して作成したパスに書き込み
コードパスの分析
問題のあるコード(プラグインインストール、約89行目):
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-clawhub-package-"));
const archivePath = path.join(tmpDir, `${params.name}.zip`);
await fs.writeFile(archivePath, bytes);スキルインストールパス(downloadClawHubSkillArchive、約232行目)にも 동일한パターンがあります:
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-clawhub-package-"));
const archivePath = path.join(tmpDir, `${params.name}.zip`);
await fs.writeFile(archivePath, bytes);パス解決の崩壊
params.nameが@axonflow/openclawの場合:
| ステップ | 操作 | 結果 |
|---|---|---|
| 1 | path.join(tmpDir, "@axonflow/openclaw.zip") | <tmpDir>/@axonflow/openclaw.zip |
| 2 | path.dirname(archivePath) | <tmpDir>/@axonflow |
| 3 | fs.mkdtemp()が作成 | <tmpDir>のみ |
| 4 | fs.writeFile()が書き込みを試行 | <tmpDir>/@axonflow/openclaw.zip |
| 5 | 親ディレクトリ@axonflowが存在しない | ENOENTがスローされる |
fs.mkdtempが中間ディレクトリを作成しない理由
fs.mkdtemp(prefix)は正確に1つのディレクトリを作成し、その絶対パスを返します。以下のことはしません:
- ディレクトリセグメントのプレフィックスを解析する
- プレフィックス文字列によって暗黙的に示されるサブディレクトリを作成する
- 返されたパスがプレフィックス構造と一致することを検証する
したがって、呼び出し结果是<tmpDir>(例:/tmp/openclaw-clawhub-package-abc123)のみですが、コードはその後<tmpDir>/@axonflow/openclaw.zipへの書き込みを試み、@axonflow/サブディレクトリが存在しない必要があります。
npmスコーピングの規則
@scope/name構文は、スコープ付きパッケージの標準的なnpm規則です。ClawHubはプラグインとスキルの識別子にこの命名スキームをサポートしているため、このバグはコミュニティパッケージすべてにとってシステム上のブロッカーとなります。
🛠️ 解決手順
オプションA: ファイル名をサニタイズする(推奨)
アーカイブパスを構築する前に、パッケージ名のスラッシュを置換します。これは、動作の変更が最小限でシンプルな修正です。
修正前:
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-clawhub-package-"));
const archivePath = path.join(tmpDir, `${params.name}.zip`);
await fs.writeFile(archivePath, bytes);修正後:
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-clawhub-package-"));
const safeName = params.name.replace(/\//g, '_');
const archivePath = path.join(tmpDir, `${safeName}.zip`);
await fs.writeFile(archivePath, bytes);これにより@axonflow/openclawが@axonflow_openclawに変換され、<tmpDir>/@axonflow_openclaw.zipという有効なパスが生成されます。
オプションB: 親ディレクトリが存在することを確認する
書き込み前にfs.mkdirとrecursiveフラグを使用して、必要な親ディレクトリを作成します。これは、他のコードがディレクトリ構造に依存している場合に有効です。
修正前:
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-clawhub-package-"));
const archivePath = path.join(tmpDir, `${params.name}.zip`);
await fs.writeFile(archivePath, bytes);修正後:
const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-clawhub-package-"));
const archivePath = path.join(tmpDir, `${params.name}.zip`);
await fs.mkdir(path.dirname(archivePath), { recursive: true });
await fs.writeFile(archivePath, bytes);これにより<tmpDir>/@axonflow/がディレクトリとして作成され、その後<tmpDir>/@axonflow/openclaw.zipが書き込まれます。
適用対象
両方の場所に修正を適用します:
- プラグインインストール: `openclaw plugins install`を処理する関数(約89行目)
- スキルインストール: `downloadClawHubSkillArchive`関数(約232行目)
ビルドとデプロイ
ソースTypeScript/JavaScriptファイルを変更した後:
# 影響を受けるモジュールを再ビルド
npm run build -- --filter=clawhub
# ビルドシステムで必要であれば、すべてのパッケージを再ビルド
npm run build
# 回帰がないことを確認するためにテストを実行
npm test -- --grep "clawhub"🧪 検証
機能検証手順
- スコープ付きプラグインのインストールをテスト:
$ openclaw plugins install @axonflow/[email protected]Resolving clawhub:@axonflow/[email protected]… ClawHub code-plugin @axonflow/[email protected] channel=community verification=source-linked Compatibility: pluginApi=>=2026.3.22 minGateway=>=2026.3.22 Downloading package archive… Extracting to plugins directory… Successfully installed plugin “@axonflow/openclaw” (version 1.2.1)
- プラグインが認識されていることを確認:
$ openclaw plugins list | grep axonflow @axonflow/openclaw 1.2.1 enabled - スコープ付きスキルのインストールをテスト(該当する場合):
$ openclaw skills install @company/[email protected] Successfully installed skill "@company/enterprise-skill" (version 2.0.0)
回帰テスト
スコープなしパッケージが引き続き動作することを確認:
$ openclaw plugins install mywallet
$ openclaw plugins list | grep mywallet
mywallet 1.5.0 enabled
$ openclaw plugins install unpkg-test
$ openclaw plugins list | grep unpkg-test
unpkg-test 0.1.0 enabled終了コードの検証
$ openclaw plugins install @axonflow/[email protected]
$ echo $?
0正常なインストールは終了コード0を返します。以前は失敗していたENOENTは非ゼロの終了コードを返していました。
ユニットテストの検証
ClawHub統合のユニットテストがコードベースに含まれる場合:
$ npm test -- --grep "downloadClawHubPlugin"
downloadClawHubPlugin
✓ should handle scoped package names (@scope/name)
✓ should handle unscoped package names
✓ should handle version specifiers
$ npm test -- --grep "downloadClawHubSkillArchive"
downloadClawHubSkillArchive
✓ should handle scoped skill names
✓ should handle nested scope names (@org/team/skill)⚠️ よくある落とし穴
環境固有のトラップ
- tmpfsを使用するDockerコンテナ: 一部のDocker構成では、`/tmp`が制限された容量のtmpfsとしてマウントされています。大きなzipアーティファクトを含むスコープ付きパッケージは、`ENOENT`ではなく`ENOSPC`で失敗する可能性があります。tmpfsの容量割り当てを確認します:
df -h /tmp # プラグインアーカイブに十分な容量を確保(デフォルトのtmpfsは多くの場合64MB) - Windowsのパス区切り文字: バグは技術的にはプラットフォームに依存しませんが、Windowsパスはパス内の`@`文字で異なる動作を示す場合があります。セキュリティに敏感なコンテキストでは、`@`を含むパスを避けます。
- tmp内のシンボリックリンク: `os.tmpdir()`がシンボリックリンクのパスに解決される場合(開発環境で一般的)、ターゲットがディレクトリの作成をサポートしていることを確認します。
ユーザーの設定ミス
- パッケージ名の形式が正しくない: ユーザーがスコープが存在する場合にスコープを省略することがあります:
# 間違い openclaw plugins install axonflow/openclaw正しい
openclaw plugins install @axonflow/openclaw
- 大文字と小文字の区別: npmスコープは、大文字と小文字が区別されます。`@Axonflow`と`@axonflow`は異なるスコープです。
- バージョンは固定されているがスコープが欠落: バージョンを指定する場合は、シェル解釈を防ぐためにパッケージ識別子全体を引用符で囲む必要があります:
# 引用符で囲むことでシェル展開の問題を防ぐ openclaw plugins install "@axonflow/[email protected]"一部のシェルでは引用符がないと、@が変数展開をトリガーする可能性がある
openclaw plugins install @axonflow/[email protected] # シェルによっては失敗する可能性がある
エッジケース
- 二重スラッシュインジェクション: `params.name`の先頭または末尾にスラッシュが含まれている場合、サニタイズ正規表現で処理する必要があります。オプションAを拡張します:
const safeName = params.name.replace(/\//g, '_').replace(/^_+|_+$/g, ''); - パッケージ名に含まれるUnicodeと絵文字: 標準的なnpmのプラクティスではありませんが、一部のレジストリでは非ASCII文字を許可しています。サポートされている場合は、国際化されたパッケージ名でテストします。
- 非常に長いパッケージ名: npmはパッケージ名を214文字に制限しています。非常に長いスコープ付き名前は、一部のプラットフォームでファイルシステムのパス名の制限(通常はパスコンポーネントあたり255バイト)に近づく可能性があります。
一時ディレクトリのクリーンアップ
fs.mkdtempによって作成された一時ディレクトリは、プロセスクラッシュまたはSIGKILL時に自動的にクリーンアップされません。クリーンアップハンドラーの追加を検討します:
process.on('SIGINT', () => {
fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
process.exit(1);
});🔗 関連するエラー
直接的な関連
ENOENT: no such file or directory— 存在しないパスへの書き込みを試みたときの標準的なエラーです。この問題は、パスに予期しないディレクトリ区切り文字が含まれている特定のケースです。ENOTDIR: not a directory— ディレクトリであるべきパスコンポーネントがファイルとして扱われた場合、またはその逆の場合に発生する可能性があります。ファイル名のサニタイズによって誤って競合が発生する場合に関連します。EISDIR: is a directory— パッケージ名のサニタイズが空文字列を生成し、結果として一時ディレクトリ自体への書き込みを試みる場合に発生する可能性があります。
類似の過去の問題
- カスタム出力ディレクトリでスコープ付きパッケージを使用したnpm install — カスタム
--prefixディレクトリにスコープ付きパッケージが含まれている場合、初期のnpm CLIバージョンで類似のパス構築問題が発生していました。 - @接頭辞付き名前のGitHub Actionsアーティファクトアップロード — アーティファクト名に
@文字が含まれている場合、アーティファクトアップロードステップが失敗していました。バックエンドが@を別のアーティファクトへの参照として解釈していたためです。 - スラッシュを含む
[name]を持つWebpackチャンクファイル名 — 解決された[name]にパス区切り文字が含まれている場合、ビルド出力はチャンクを書き込めず、同じサニタイズ修正が必要였습니다。
影響を受けるClawHubパッケージパターン
このバグは、ClawHubエコシステム全体の特定の命名規則パターンに影響します:
# スコープ付きパッケージ(壊れている)
@axonflow/openclaw
@company/workflow-engine
@org/team/shared-utilities
# スコープなしパッケージ(動作中)
mywallet
workflow-engine
shared-utilitiesnpm標準の@scope/name規則を使用するすべてのパケットは、修正がデプロイされるまで影響を受けます。
監視と検出
本番ログでこの問題を検出するには:
# 特定のENOENTパターンのログをフィルター
grep "ENOENT.*openclaw-clawhub-package.*@.*\.zip" /var/log/openclaw/*.log
# 時間枠ごとの影響を受けるインストール数をカウント
grep "clawhub.*ENOENT" /var/log/openclaw/*.log | \
awk '{print $4}' | sort | uniq -c | sort -rn | head -20