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
- Use a plain SSH URL instead: git@host:owner/repo.git or ssh://git@host/owner/repo.git
- For proxy needs, configure ~/.ssh/config (ProxyCommand) rather than the ext transport
- 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
- Normalize git URLs to https:// or git@host: forms in your tooling
- Reject ext::, and generally transport-executing URLs, at input validation
- Put proxy configuration in ~/.ssh/config instead of URL transports
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
- Archive contains too many files (${state.entries}). Maximum
- Archive contains unsafe path: ${path}
- Archive contains unsafe path: ${entryPath}
- Clone timed out after ${seconds}s. Common causes: - Large
- buildGitHubAuthError(url, repo, errorMessage)
AI-assisted analysis of vercel-labs/skills@435076e789 (2026-08-28).
Data as JSON: /api/errors/95e845c2f7c62957.
Report an issue: GitHub.