neoclide/coc.nvim · error

Invalid extension name: ${name}

Error message

Invalid extension name: ${name}

What it means

extensionPath validates npm package names before touching the filesystem: the name must be a single path component or a scoped '@scope/pkg' name, without path separators, NUL bytes, '.'/'..' segments, or traversal. The first regex/NUL check throws for syntactically invalid names to prevent registry-controlled metadata from driving destructive filesystem operations.

Source

Thrown at src/extension/installer.ts:18

'use strict'
import { EventEmitter } from 'events'
import { createLogger } from '../logger'
import download, { DownloadOptions } from '../model/download'
import fetch, { FetchOptions } from '../model/fetch'
import { loadJson } from '../util/fs'
import { child_process, fs, minimatch, os, path, readline, semver } from '../util/node'
import { toText } from '../util/string'
import workspace from '../workspace'
const logger = createLogger('extension-installer')
const local_dependencies = ['coc.nvim', 'esbuild', 'webpack', '@types/node']

function extensionPath(root: string, name: string | undefined): string {
  // npm package names contain either one path component, or two for a scoped
  // package.  Reject path syntax before using registry-controlled metadata in
  // destructive filesystem operations.
  if (typeof name !== 'string' || !/^(?:@[^/\\]+\/)?[^/\\]+$/.test(name) || name.includes('\0') || name.split('/').some(part => part === '.' || part === '..')) {
    throw new Error(`Invalid extension name: ${name}`)
  }
  let resolvedRoot = path.resolve(root)
  let target = path.resolve(resolvedRoot, name)
  let relative = path.relative(resolvedRoot, target)
  if (!relative || relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
    throw new Error(`Invalid extension name: ${name}`)
  }
  return target
}

export interface Info {
  'dist.tarball'?: string
  'engines.coc'?: string
  version?: string
  name?: string
}

export type Dependencies = Record<string, string>

View on GitHub (pinned to 50e974d969)

Solutions

  1. Validate/normalize the extension name before calling: single component or '@scope/pkg'.
  2. Strip version suffixes and URI encoding before passing the name.
  3. Ensure the name is a non-empty string (guard undefined).
  4. If the source is a URI, parse it properly to extract just the package name.

Example fix

// before
let info = await installer.getInfo('coc-tsserver/../evil') // throws
// after
let name = 'coc-tsserver' // clean npm name: /^(?:@[^/\\]+\/)?[^/\\]+$/
let info = await installer.getInfo(name)
Defensive patterns

Strategy: validation

Validate before calling

function isValidExtName(name: unknown): name is string {
  return typeof name === 'string' &&
    /^(?:@[^/\\]+\/)?[^/\\]+$/.test(name) &&
    !name.includes('\0') &&
    !name.split('/').some(p => p === '.' || p === '..')
}

Type guard

function isExtensionName(v: unknown): v is string {
  return typeof v === 'string' && /^(?:@[^/\\]+\/)?[^/\\]+$/.test(v)
}

Try / catch

try {
  const p = extensionPath(root, name)
} catch (e) {
  if (e.message.startsWith('Invalid extension name')) {
    throw new UserInputError(`'${name}' is not a valid npm/coc package name`)
  } else throw e
}

Prevention

When it happens

Trigger: Calling extensionPath (directly or via getInfo, getInfoFromUri, install, folder, dest) with name = undefined, an empty string, a name containing '/' or '\' beyond scope syntax, embedded NUL, or '.'/'..' segments.

Common situations: Parsing a malformed coc-extension URI; user typed 'coc-tsserver/' or '@scope/' in an install command; split on '/' producing empty parts; programmatic callers passing unparsed 'pkg@version' strings with slashes.

Related errors


AI-assisted analysis of neoclide/coc.nvim@50e974d969 (2026-08-31). Data as JSON: /api/errors/c0cb41c596567f8c. Report an issue: GitHub.