shadcn-ui/ui · error · Error

Invalid target path " ". Target paths using @ / must stay…

Error message

Invalid target path "${target}". Target paths using @${aliasKey}/ must stay within the ${aliasKey} alias root.

What it means

Thrown by resolveAliasTarget when a file target of the form @aliasKey/subpath resolves to a path outside the alias root. After matching ^@([^/]+)/(.+)$ and confirming aliasKey is a known target alias, it computes aliasRoot = config.resolvedPaths[aliasKey] and resolvedPath = resolve(aliasRoot, targetPath); if resolvedPath is neither the aliasRoot itself nor beneath it, the target escapes and is rejected.

Solutions

  1. Edit the offending target to remove ".." segments so it stays inside the alias root.
  2. Use a target that the alias root legitimately contains, e.g. "@components/ui/button.tsx".
  3. If you need a file in a different alias, switch the target to that alias's prefix.
  4. Re-run the add command after the registry item is fixed.

Example fix

// registry item (before)
{ "path": "x.ts", "target": "@components/../lib/x.ts", "type": "registry:ui" }
// after
{ "path": "x.ts", "target": "@lib/x.ts", "type": "registry:ui" }
Defensive patterns

Strategy: validation

Validate before calling

import path from "path"
function assertAliasTargetInside(target: string, aliasRoot: string) {
  const resolved = path.resolve(aliasRoot, target.replace(/^@[^/]+\//, ""))
  if (resolved !== aliasRoot && !resolved.startsWith(aliasRoot + path.sep)) {
    throw new Error(`target escapes alias root: ${target}`)
  }
}

Type guard

const isAliasTargetInside = (target: string, aliasRoot: string) => {
  const m = target.match(/^@([^/]+)\/(.+)$/)
  if (!m) return true
  const resolved = path.resolve(aliasRoot, m[2])
  return resolved === aliasRoot || resolved.startsWith(aliasRoot + path.sep)
}

Try / catch

try {
  await updateFiles(tree, config, options)
} catch (e) {
  if (e instanceof Error && /must stay within the .* alias root/.test(e.message)) {
    // rewrite the target to remove ".." and retry
  }
  throw e
}

Prevention

When it happens

Trigger: A registry file uses target like "@components/../lib/secret" so that path.resolve pushes it above the components alias root. The containment check `resolvedPath !== aliasRoot && !resolvedPath.startsWith(aliasRoot + sep)` fails and throws.

Common situations: Hand-written target with "../" that climbs out of the alias directory; refactoring an alias root without updating targets; malicious registry trying to write outside the alias.

Related errors


AI-assisted analysis of shadcn-ui/ui@efac598707 (2026-08-12). Data as JSON: /api/errors/6d85eac41b27627a. Report an issue: GitHub.

Appendix: source

Thrown at packages/shadcn/src/utils/updaters/update-files.ts:475

    return null
  }

  const [, aliasKey, targetPath] = match

  if (!isTargetAliasKey(aliasKey)) {
    return {
      target: `${aliasKey}/${targetPath}`,
    }
  }

  const aliasRoot = path.resolve(config.resolvedPaths[aliasKey])
  const resolvedPath = path.resolve(aliasRoot, targetPath)

  if (
    resolvedPath !== aliasRoot &&
    !resolvedPath.startsWith(`${aliasRoot}${path.sep}`)
  ) {
    throw new Error(
      `Invalid target path "${target}". Target paths using @${aliasKey}/ must stay within the ${aliasKey} alias root.`
    )
  }

  return {
    resolvedPath,
  }
}

function resolveFileTargetDirectory(
  file: z.infer<typeof registryItemFileSchema>,
  config: Config
) {
  if (file.type === "registry:ui") {
    return config.resolvedPaths.ui
  }

  if (file.type === "registry:lib") {

View on GitHub (pinned to efac598707)