chatboxai/chatbox · critical · Error
Script path escapes skills directory
Error message
Script path escapes skills directory
What it means
Thrown by the skills:execute-script IPC handler when fs.realpathSync resolves both the skills directory and the script path, and the resolved script path does not start with the resolved skills directory plus a path separator. This is the backstop against symlink-based escapes: even if skillName/scriptName pass the string checks (104/105), a symlink inside the skills tree pointing outside is caught here.
Source
Thrown at src/main/skills/ipc-handlers.ts:193
}
if (skillName.includes('..') || skillName.includes('/') || skillName.includes('\\')) {
throw new Error('Invalid skill name: path traversal not allowed')
}
if (scriptName.includes('..') || scriptName.includes('/') || scriptName.includes('\\')) {
throw new Error('Invalid script name: path traversal not allowed')
}
const skillsDir = getSkillsDir()
const scriptPath = path.join(skillsDir, skillName, 'scripts', scriptName)
if (!fs.existsSync(scriptPath)) {
throw new Error(`Script not found: ${scriptName}`)
}
const resolvedSkillsDir = fs.realpathSync(skillsDir)
const resolvedScriptPath = fs.realpathSync(scriptPath)
if (!resolvedScriptPath.startsWith(`${resolvedSkillsDir}${path.sep}`)) {
throw new Error('Script path escapes skills directory')
}
const scriptDir = path.dirname(resolvedScriptPath)
return await new Promise((resolve) => {
const TIMEOUT_MS = 30_000
let stdout = ''
let stderr = ''
let settled = false
const resolveOnce = (result: {
success: boolean
stdout: string
stderr: string
exitCode: number | null
}) => {
if (settled) {
returnView on GitHub (pinned to 81571269ad)
Solutions
- Audit installed skills for symlinks under skills/{name}/scripts/ and remove any that resolve outside the skills directory.
- Ensure the skills directory path passed to getSkillsDir() is itself canonical (run realpathSync on it at startup).
- If the skills dir legitimately lives under a symlinked parent, store its realpath once and compare against that consistently.
Example fix
// before
const resolvedSkillsDir = fs.realpathSync(skillsDir)
const resolvedScriptPath = fs.realpathSync(scriptPath)
if (!resolvedScriptPath.startsWith(`${resolvedSkillsDir}${path.sep}`)) {
throw new Error('Script path escapes skills directory')
}
// after — use path.relative for a robust containment check that handles trailing separators
const rel = path.relative(resolvedSkillsDir, resolvedScriptPath)
if (rel.startsWith('..') || path.isAbsolute(rel)) {
throw new Error('Script path escapes skills directory')
} Defensive patterns
Strategy: validation
Validate before calling
import fs from 'node:fs'
import path from 'node:path'
function isScriptContained(skillsDir: string, scriptPath: string): boolean {
const resolvedSkillsDir = fs.realpathSync(skillsDir)
const resolvedScriptPath = fs.realpathSync(scriptPath)
const rel = path.relative(resolvedSkillsDir, resolvedScriptPath)
return !rel.startsWith('..') && !path.isAbsolute(rel)
} Try / catch
const result = await ipcRenderer.invoke('skills:execute-script', params)
if (!result.success && /escapes skills directory/i.test(result.stderr)) {
showToast('This skill contains a symlink that is not allowed')
reportSuspiciousSkill(skillName)
} Prevention
- Run an audit pass after install that rejects any symlink under scripts/ resolving outside skills dir.
- Canonicalize the skills directory path once at startup with realpathSync and compare consistently.
- Use path.relative() for containment checks — it is more robust than startsWith with separator concatenation.
When it happens
Trigger: skills/{skillName}/scripts/{scriptName} is a symlink whose target resolves to a path outside realpathSync(skillsDir). This happens when a malicious skill installs a symlink chain, or when the skills directory itself is a symlink and realpathSync normalizes it differently than expected (e.g. on macOS /tmp is a symlink to /private/tmp).
Common situations: A crafted skill bundle includes scripts/run.sh -> /usr/bin/id; the user's skills directory lives under a symlinked parent (e.g. ~/.config is a symlink) and the separator logic mishandles the boundary; a skill was installed by copying a directory whose scripts were themselves symlinks into system paths.
Related errors
- Invalid skill name: path traversal not allowed
- Invalid script name: path traversal not allowed
- invalid file path for "${name}": ${file.path}
- Script not found: ${scriptName}
- Skill name and script name are required
AI-assisted analysis of chatboxai/chatbox@81571269ad (2026-08-12).
Data as JSON: /api/errors/cfef92bd90ac5967.
Report an issue: GitHub.