vercel-labs/skills · critical · GitCloneError

Unsupported Git transport: ext

Error message

Unsupported Git transport: ext

What it means

cloneRepo() rejects any Git URL using the ext:: transport before spawning git. The ext transport executes arbitrary shell commands via local helpers and is a well-known vector for command injection, so the skills CLI hard-blocks it.

Source

Thrown at src/git.ts:237

    return (
      `Authentication failed for ${url}.\n` +
      `  - For private repos, ensure you have access\n` +
      `  - Retry with SSH: npx skills add ${repo.sshUrl}\n` +
      `  - Check access with: gh auth status -h ${host} or ssh -T git@${host}`
    );
  }

  return (
    `Authentication failed for ${url}.\n` +
    `  - For private repos, ensure you have access\n` +
    `  - For SSH: Check your keys with 'ssh -T git@github.com'\n` +
    `  - For HTTPS: Run 'gh auth login' or configure git credentials`
  );
}

export async function cloneRepo(url: string, ref?: string): Promise<string> {
  if (/^ext::/i.test(url)) {
    throw new GitCloneError('Unsupported Git transport: ext', url);
  }

  const tempDir = await mkdtemp(join(tmpdir(), 'skills-'));
  const cloneOptions = ref ? ['--depth', '1', '--branch', ref] : ['--depth', '1'];
  const repo = parseGitHubRepoUrl(url);

  try {
    await createGitClient().clone(url, tempDir, cloneOptions);
    return tempDir;
  } catch (error) {
    const errorMessage = error instanceof Error ? error.message : String(error);
    const isTimeout = errorMessage.includes('block timeout') || errorMessage.includes('timed out');
    const isAuthError = isAuthFailure(errorMessage);

    if (isTimeout) {
      await rm(tempDir, { recursive: true, force: true }).catch(() => {});
      const seconds = Math.round(CLONE_TIMEOUT_MS / 1000);
      throw new GitCloneError(

View on GitHub (pinned to 435076e789)

Solutions

  1. Use a plain SSH URL instead: git@host:owner/repo.git or ssh://git@host/owner/repo.git
  2. For proxy needs, configure ~/.ssh/config (ProxyCommand) rather than the ext transport
  3. Never pass ext:: URLs to this CLI — there is no flag to allow them by design

Example fix

# before
skills add "ext::ssh -i /key git@git.corp:team/skills.git"
# after
skills add git@git.corp:team/skills.git   # with ProxyCommand in ~/.ssh/config
Defensive patterns

Strategy: validation

Validate before calling

function isSafeGitUrl(url: string): boolean {
  return !/^ext::/i.test(url) && /^(https?|git|ssh|file):|^[\w.-]+@[\w.-]+:/.test(url);
}
if (!isSafeGitUrl(source)) throw new Error(`Blocked unsafe git transport: ${source}`);

Type guard

function isUnsupportedTransport(e: unknown): e is Error & { url?: string } {
  return e instanceof Error && /Unsupported Git transport/i.test(e.message);
}

Try / catch

try { await cloneRepo(url); }
catch (e) {
  if (isUnsupportedTransport(e)) {
    throw new Error(`Convert ${url} to an ssh:// URL; ext:: is forbidden for security`);
  }
  throw e;
}

Prevention

When it happens

Trigger: Passing a source like 'ext::ssh -iKey git@server repo' (or any URL matching /^ext::/i) to cloneRepo — via 'skills add <url>', 'skills use', or an update flow that clones.

Common situations: Users copying exotic Git transport URLs from internal docs or older automation scripts; CI configurations that historically used ext:: wrappers for SSH proxying.

Related errors


AI-assisted analysis of vercel-labs/skills@435076e789 (2026-08-28). Data as JSON: /api/errors/95e845c2f7c62957. Report an issue: GitHub.