upstash/context7 · error · Error
Unsafe skill name: ${JSON.stringify(skillName)}
Error message
Unsafe skill name: ${JSON.stringify(skillName)} What it means
Thrown by assertSkillNameInRoot() when isSafeSkillName() rejects the name. A safe name must be a string of length 1–128, not '.' or '..', contain no NUL byte, and match ^[a-zA-Z0-9][a-zA-Z0-9._-]*$ (alphanumeric first char; only letters, digits, '.', '_', '-' afterwards). The error stringifies the name with JSON.stringify so control characters are visible.
Source
Thrown at packages/cli/src/utils/skill-name.ts:16
import { resolve, dirname, basename } from "path";
const SAFE_NAME = /^[a-zA-Z0-9][a-zA-Z0-9._-]*$/;
export function isSafeSkillName(name: string): boolean {
if (typeof name !== "string") return false;
if (name.length === 0 || name.length > 128) return false;
if (name === "." || name === "..") return false;
if (name.includes("\0")) return false;
if (!SAFE_NAME.test(name)) return false;
return true;
}
export function assertSkillNameInRoot(skillsRoot: string, skillName: string): string {
if (!isSafeSkillName(skillName)) {
throw new Error(`Unsafe skill name: ${JSON.stringify(skillName)}`);
}
const root = resolve(skillsRoot);
const target = resolve(root, skillName);
if (dirname(target) !== root || basename(target) !== skillName) {
throw new Error(`Skill name "${skillName}" escapes the skills root`);
}
return target;
}
View on GitHub (pinned to ca15df0443)
Solutions
- Rename to start with an alphanumeric character and use only a-zA-Z0-9._- afterwards.
- Strip leading/trailing whitespace and any '/', '\\', '@', ':' from the name.
- Shorten to <= 128 characters.
- For scoped skills, flatten '@org/skill' to 'org.skill' or 'org-skill'.
Example fix
// before assertSkillNameInRoot(root, "@upstash/context7"); // throws: contains '@' and '/' // after assertSkillNameInRoot(root, "upstash-context7");
Defensive patterns
Strategy: validation
Validate before calling
import { isSafeSkillName } from "../utils/skill-name";
function normalizeSkillName(raw: string): string {
const trimmed = raw.trim().replace(/^[@]/, "").replace(/[\\/]+/g, "-");
return trimmed;
}
const name = normalizeSkillName(input);
if (!isSafeSkillName(name)) {
throw new Error(`"${input}" is not a valid skill name (use a-z0-9 and ._-, max 128 chars).`);
}
assertSkillNameInRoot(skillsRoot, name); Type guard
// Direct reuse of the library predicate as a userland type guard.
import { isSafeSkillName } from "../utils/skill-name";
function asSkillName(raw: unknown): string | null {
return typeof raw === "string" && isSafeSkillName(raw) ? raw : null;
} Try / catch
try {
assertSkillNameInRoot(skillsRoot, candidate);
} catch (e) {
const msg = (e as Error).message;
if (msg.startsWith("Unsafe skill name")) {
throw new Error(`Refusing to install: ${msg}. Use only a-z0-9 and ._- characters.`);
}
throw e;
} Prevention
- Validate skill names at the input boundary with isSafeSkillName before any filesystem work.
- Flatten registry-style ids ('@org/skill') to 'org-skill' before passing them in.
- Trim whitespace and reject blank strings explicitly.
When it happens
Trigger: Empty string; name longer than 128 chars; name with '/', '\\', ':', space, or other punctuation; name starting with '.' or '-' or '_' (first char must be alphanumeric); name containing a NUL byte; non-string input (number/object).
Common situations: User typed a scoped registry id like '@org/skill' (contains '@' and '/'); copy-paste included a trailing slash or whitespace; name derived from a filesystem path with separators; very long auto-generated slug.
Related errors
- Skill file path "${file.path}" resolves outside the target d
- Skill name "${skillName}" escapes the skills root
- no files
- query and libraryName are required
AI-assisted analysis of upstash/context7@ca15df0443 (2026-08-12).
Data as JSON: /api/errors/028ce783c8d58c72.
Report an issue: GitHub.