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) {
              return

View on GitHub (pinned to 81571269ad)

Solutions

  1. Audit installed skills for symlinks under skills/{name}/scripts/ and remove any that resolve outside the skills directory.
  2. Ensure the skills directory path passed to getSkillsDir() is itself canonical (run realpathSync on it at startup).
  3. 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

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


AI-assisted analysis of chatboxai/chatbox@81571269ad (2026-08-12). Data as JSON: /api/errors/cfef92bd90ac5967. Report an issue: GitHub.