Scoped npm Packages from ClawHub Fail to Install with ENOENT
Plugins and skills published with npm-style scoped names (@scope/package) throw ENOENT errors during installation because the temporary archive path contains unresolved directory segments.
๐ Symptoms
Primary Error Manifestation
Attempting to install any scoped npm package from ClawHub results in an ENOENT error during the archive write phase:
$ 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'
Diagnostic Observations
- Non-scoped packages succeed: Packages without a scope segment install without error
$ openclaw plugins install mywallet # Output: Successfully installed plugin "mywallet"- Error persists across versions: All `@scope/name` patterns trigger the failure regardless of the actual scope or package name
- Exit code: Process terminates with exit code
ENOENT(numeric equivalent:34)
Error Object Details
The JavaScript error object provides these properties:
{
errno: -2,
code: 'ENOENT',
syscall: 'open',
path: '/var/folders/.../openclaw-clawhub-package-XXXXXX/@axonflow/openclaw.zip'
}The critical observation is that the path contains a forward slash in the filename segment, which POSIX path resolution interprets as a directory separator.
๐ง Root Cause
Architectural Failure Sequence
The installation pipeline in dist/clawhub-CFvPS51z.js follows this sequence:
- Create a unique temporary directory using
fs.mkdtemp() - Construct the archive path by joining the temp directory with the package name
- Write the downloaded zip bytes directly to the constructed path
Code Path Analysis
The problematic code (plugin installation, approximate line 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);The identical pattern exists in the skills installation path (downloadClawHubSkillArchive, approximate line 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);Path Resolution Breakdown
When params.name equals @axonflow/openclaw:
| Step | Operation | Result |
|---|---|---|
| 1 | path.join(tmpDir, "@axonflow/openclaw.zip") | <tmpDir>/@axonflow/openclaw.zip |
| 2 | path.dirname(archivePath) | <tmpDir>/@axonflow |
| 3 | fs.mkdtemp() creates | <tmpDir> only |
| 4 | fs.writeFile() attempts to write | <tmpDir>/@axonflow/openclaw.zip |
| 5 | Parent directory @axonflow does not exist | ENOENT thrown |
Why fs.mkdtemp Does Not Create Intermediate Directories
fs.mkdtemp(prefix) creates exactly one directory and returns its absolute path. It does not:
- Parse the prefix for directory segments
- Create any subdirectories implied by the prefix string
- Validate that the returned path matches the prefix structure
Therefore, the call produces <tmpDir> (e.g., /tmp/openclaw-clawhub-package-abc123), but the code subsequently attempts to write to <tmpDir>/@axonflow/openclaw.zip, which requires a non-existent @axonflow/ subdirectory.
npm Scoping Convention
The @scope/name syntax is the standard npm convention for scoped packages. ClawHub supports this naming scheme for plugin and skill identifiers, making this bug a systemic blocker for all community packages using this convention.
๐ ๏ธ Step-by-Step Fix
Option A: Sanitize the Filename (Recommended)
Replace forward slashes in the package name before constructing the archive path. This is the simpler fix with minimal behavioral change.
Before:
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);After:
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);This converts @axonflow/openclaw to @axonflow_openclaw, producing the valid path <tmpDir>/@axonflow_openclaw.zip.
Option B: Ensure Parent Directory Exists
Use fs.mkdir with the recursive flag to create any necessary parent directories before writing. This preserves the full directory structure if other code depends on it.
Before:
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);After:
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);This creates <tmpDir>/@axonflow/ as a directory, then writes <tmpDir>/@axonflow/openclaw.zip.
Application Targets
Apply the fix to both locations:
- Plugin installation: Function handling
openclaw plugins install(around line 89) - Skill installation:
downloadClawHubSkillArchivefunction (around line 232)
Build and Deploy
After modifying the source TypeScript/JavaScript files:
# Rebuild the affected module
npm run build -- --filter=clawhub
# Or rebuild all packages if build system requires it
npm run build
# Run tests to verify no regressions
npm test -- --grep "clawhub"๐งช Verification
Functional Verification Steps
- Test scoped plugin installation:
$ 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)
- Verify plugin is recognized:
$ openclaw plugins list | grep axonflow @axonflow/openclaw 1.2.1 enabled - Test scoped skill installation (if applicable):
$ openclaw skills install @company/[email protected] Successfully installed skill "@company/enterprise-skill" (version 2.0.0)
Regression Testing
Confirm non-scoped packages continue to work:
$ 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 enabledExit Code Verification
$ openclaw plugins install @axonflow/[email protected]
$ echo $?
0A successful installation returns exit code 0. The previously failing ENOENT would have returned a non-zero exit code.
Unit Test Validation
If the codebase includes unit tests for the ClawHub integration:
$ 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)โ ๏ธ Common Pitfalls
Environment-Specific Traps
- Docker containers with tmpfs: Some Docker configurations mount
/tmpas tmpfs with limited space. Scoped packages with large zip artifacts may fail withENOSPCrather thanENOENT. Verify tmpfs size allocation:df -h /tmp # Ensure adequate space for plugin archives (default tmpfs often 64MB) - Windows path separators: While the bug is technically platform-agnostic, Windows paths may exhibit different behavior with
@characters in paths. Avoid paths containing@in security-sensitive contexts. - Symbolic links in tmp: If
os.tmpdir()resolves to a symlinked path (common in development environments), ensure the target supports directory creation.
User Misconfigurations
- Incorrect package name format: Users sometimes omit the scope when it exists:
# Wrong openclaw plugins install axonflow/openclawCorrect
openclaw plugins install @axonflow/openclaw
- Case sensitivity: npm scopes are case-sensitive.
@Axonflowand@axonfloware distinct scopes. - Version pinned but scope missing: When specifying versions, ensure the entire package identifier is quoted to prevent shell interpretation:
# Quotes prevent shell expansion issues openclaw plugins install "@axonflow/[email protected]"Without quotes on some shells, @ may trigger variable expansion
openclaw plugins install @axonflow/[email protected] # May fail depending on shell
Edge Cases
- Double-slash injection: If
params.namecontains leading or trailing slashes, the sanitization regex should handle them. Extend Option A:const safeName = params.name.replace(/\//g, '_').replace(/^_+|_+$/g, ''); - Unicode and emoji in package names: While not standard npm practice, some registries allow non-ASCII characters. Test with internationalized package names if supported.
- Extremely long package names: npm limits package names to 214 characters. Very long scoped names may approach filesystem path length limits on some platforms (typically 255 bytes per path component).
Temporary Directory Cleanup
The temporary directories created by fs.mkdtemp are not automatically cleaned up on process crash or SIGKILL. Consider adding cleanup handlers:
process.on('SIGINT', () => {
fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});
process.exit(1);
});๐ Related Errors
Directly Related
ENOENT: no such file or directoryโ The canonical error when attempting to write to a non-existent path. This issue is a specific instance where the path contains unexpected directory separators.ENOTDIR: not a directoryโ May occur if a path component that should be a directory is treated as a file, or vice versa. Related when the filename sanitization inadvertently creates conflicts.EISDIR: is a directoryโ Could occur if package name sanitization produces an empty string, resulting in attempting to write to the temp directory itself.
Similar Historical Issues
- npm install with scoped packages in custom output directories โ Similar path construction issues affected early npm CLI versions when specifying custom
--prefixdirectories containing scoped packages. - GitHub Actions artifact upload with @-prefixed names โ Artifact upload step would fail when artifact names contained
@characters, as the backend interpreted@as a reference to another artifact. - Webpack chunk filename with
[name]containing slashes โ Build output would fail to write chunks when the resolved[name]contained path separators, requiring the same sanitization fix.
Affected ClawHub Package Pattern
This bug impacts a specific naming convention pattern across the ClawHub ecosystem:
# Scoped packages (BROKEN)
@axonflow/openclaw
@company/workflow-engine
@org/team/shared-utilities
# Unscoped packages (WORKING)
mywallet
workflow-engine
shared-utilitiesAny package using the npm standard @scope/name convention is affected until the fix is deployed.
Monitoring and Detection
To detect this issue in production logs:
# Filter logs for the specific ENOENT pattern
grep "ENOENT.*openclaw-clawhub-package.*@.*\.zip" /var/log/openclaw/*.log
# Count affected installations per time window
grep "clawhub.*ENOENT" /var/log/openclaw/*.log | \
awk '{print $4}' | sort | uniq -c | sort -rn | head -20