affaan-m/ECC · error · Error
Path traversal rejected
Error message
Path traversal rejected: ${relPath} What it means
Identical traversal guard to observe-runner.js but in scripts/hooks/plugin-hook-bootstrap.js: it resolves the requested path against the plugin root and throws if the resolved target is neither the root itself nor a descendant. Prevents bootstrap logic from being pointed at files outside the plugin directory.
Solutions
- Place the referenced script/file inside the plugin root and use a root-relative path
- Remove `..` segments or absolute paths from the hook configuration
- Update the plugin's rootDir if the legitimate root changed
- Check where symlinks resolve; the canonical path must stay under rootDir
Example fix
// before resolveTarget(pluginRoot, '../bin/helper.js') // after resolveTarget(pluginRoot, 'bin/helper.js')
Defensive patterns
Strategy: validation
Validate before calling
const path = require('path');
function isInsidePluginRoot(rootDir, relPath) {
const root = path.resolve(rootDir);
const target = path.resolve(rootDir, relPath);
return target === root || target.startsWith(root + path.sep);
}
// guard scriptPath/result before calling resolveTarget Type guard
const isSafePluginPath = (p) => typeof p === 'string' && !path.isAbsolute(p) && !p.split(path.sep).includes('..'); Try / catch
try {
const resolved = resolveTarget(pluginRoot, scriptPath);
} catch (err) {
if (String(err.message).startsWith('Path traversal rejected:')) {
console.error(`Hook script outside plugin root: ${scriptPath}`);
return null;
}
throw err;
} Prevention
- Keep all hook scripts inside the plugin directory
- Regenerate hook configs when installing on a new machine instead of copying absolute paths
- Resolve symlinks in CI and assert they stay under the plugin root
When it happens
Trigger: A hook command or configured scriptPath containing `../` segments or an absolute path outside the plugin root; symlinked script locations that canonicalize outside the root.
Common situations: Copying hook configs between machines with different layouts, referencing a globally installed script path, nested plugins so relative paths resolve unexpectedly.
Understand the failure class
Background: Path traversal blocked: "path escapes the workspace" and "outside site root" errors when a path will not stay inside its allowed directory — this error's family across 26 libraries.
Related errors
- Path traversal rejected
- artifact path escapes output directory
- File path contains unsafe shell characters
- Invalid ECC repo root: missing package.json at
- output path contains an invalid component
AI-assisted analysis of affaan-m/ECC@8321021c54 (2026-09-16).
Data as JSON: /api/errors/73c7eb6dc896869f.
Report an issue: GitHub.
Appendix: source
Thrown at scripts/hooks/plugin-hook-bootstrap.js:102
}
const match = rootDir.match(/^\/([a-zA-Z])(?:\/(.*))?$/);
if (!match) {
return rootDir;
}
const [, driveLetter, rest = ''] = match;
return `${driveLetter.toUpperCase()}:/${rest}`;
}
function resolveTarget(rootDir, relPath) {
const resolvedRoot = path.resolve(rootDir);
const resolvedTarget = path.resolve(rootDir, relPath);
if (
resolvedTarget !== resolvedRoot &&
!resolvedTarget.startsWith(resolvedRoot + path.sep)
) {
throw new Error(`Path traversal rejected: ${relPath}`);
}
return resolvedTarget;
}
let _cachedShell = undefined;
let _cachedBash = undefined;
function isPowerShellBin(bin) {
const base = path.basename(bin).toLowerCase();
return base === 'pwsh.exe' || base === 'pwsh' || base === 'powershell.exe' || base === 'powershell';
}
function findShellBinary() {
if (_cachedShell !== undefined) return _cachedShell;
const candidates = [];
// Explicit override always wins — check before any platform probing.View on GitHub (pinned to 8321021c54)