santifer/career-ops · error · Error
clone of @ failed
Error message
clone of ${url}@${sha.slice(0, 10)} failed — ${err.stderr ? String(err.stderr).slice(0, 200) : err.message} What it means
safeClone wraps every git invocation (init, remote add, fetch --depth 1 of the pinned sha, checkout) and, on failure, deletes the temp dir and re-throws a message containing the URL, the first 10 sha chars, and up to 200 chars of git's stderr (or the error message). It turns low-level execFileSync failures into one actionable, rate-limit-aware message.
Solutions
- Read the embedded stderr segment — it names the concrete cause (auth, DNS, not-found, timeout)
- Verify the URL and sha exist: `git ls-remote <url> <sha>`
- Check network/proxy access to github.com and retry; on CI ensure git is installed
- If the sha was force-pushed away, pick a new commit and re-pin
Example fix
// before
const dir = safeClone('https://github.com/acme/career-ops-plugin-demo', sha); // throws opaque exec errors
// after
try {
const dir = safeClone(url, sha);
} catch (e) {
console.error(e.message); // 'clone of https://...@a1b2c3d4e5 failed — fatal: could not read Username...'
// inspect stderr, check network, or verify the sha exists
} Defensive patterns
Strategy: retry
Validate before calling
import { execFileSync } from 'child_process'; try { execFileSync('git', ['ls-remote', url, sha], { stdio: 'ignore' }); } catch { throw new Error(`unreachable repo or sha: ${url}@${sha}`); } Try / catch
let dir; for (let attempt = 1; attempt <= 3 && !dir; attempt++) { try { dir = safeClone(url, sha); } catch (e) { if (attempt === 3 || /not found|could not read/i.test(e.message)) throw e; await sleep(2 ** attempt * 500); } } Prevention
- Pre-verify reachability with git ls-remote before cloning
- Install git in CI/container images
- Retry with backoff on transient network errors; do not retry on not-found
- Watch for GitHub rate limits; authenticate or slow down bulk installs
When it happens
Trigger: safeClone called when git is missing/not on PATH, the repo/SHA does not exist (fetch fails), the network is down or a proxy blocks github.com, GitHub is rate-limiting/unavailable, or the 120s timeout expires.
Common situations: Offline or corporate proxy blocks git; typo'd owner/repo after URL validation passed; pinned sha was garbage-collected or never fetched (force-pushed away); no SSH/network access from CI; git not installed in a slim container image.
Understand the failure class
Background: "git command failed": what it means when a tool shells out to git and git exits non-zero — this error's family across 21 libraries.
Related errors
- timed out after s. If your network is slow, retry or set to…
- a 40-hex commit --sha is required
- Access denied: Egress guard blocked private target IP
- Access denied: Egress guard blocked private target IP
- addPaths received directory pathspec(s), which -f would…
AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16).
Data as JSON: /api/errors/087a82e8a899cdba.
Report an issue: GitHub.
Appendix: source
Thrown at plugin-install.mjs:54
if (!m) throw new Error(`repo must be named "career-ops-plugin-<name>" (got "${repoName}")`);
return { url, id: m[1] };
}
/** Clone the EXACT pinned SHA into a fresh temp dir. Returns the temp dir path. */
export function safeClone(url, sha) {
if (!SHA_RE.test(sha || '')) throw new Error(`a 40-hex commit --sha is required (got ${JSON.stringify(sha)})`);
const dir = mkdtempSync(path.join(tmpdir(), 'co-plugin-'));
const git = (...args) => execFileSync('git', ['-c', 'protocol.ext.allow=never', '-c', 'protocol.file.allow=never', ...args], { stdio: ['ignore', 'ignore', 'pipe'], timeout: 120_000 });
try {
git('-C', dir, 'init', '-q');
git('-C', dir, 'remote', 'add', 'origin', '--', url);
git('-C', dir, 'fetch', '--depth', '1', '--no-tags', '-q', 'origin', sha);
git('-C', dir, 'checkout', '-q', 'FETCH_HEAD');
rmSync(path.join(dir, '.git'), { recursive: true, force: true }); // drop VCS metadata (and any hooks)
return dir;
} catch (err) {
rmSync(dir, { recursive: true, force: true });
throw new Error(`clone of ${url}@${sha.slice(0, 10)} failed — ${err.stderr ? String(err.stderr).slice(0, 200) : err.message}`);
}
}
/** Check the minimum file set + a valid manifest whose id matches `expectId`. */
export function validateInstall(dir, expectId) {
const problems = [];
for (const f of MIN_FILES) if (!existsSync(path.join(dir, f))) problems.push(`missing required file: ${f}`);
if (problems.length) return { ok: false, problems, manifest: null };
let parsed;
try { parsed = JSON.parse(readFileSync(path.join(dir, 'manifest.json'), 'utf8')); }
catch (e) { return { ok: false, problems: [`manifest.json invalid JSON: ${e.message}`], manifest: null }; }
// validateManifest wants the dir to BE the plugin dir + the basename to equal id.
const tmpNamed = path.join(path.dirname(dir), expectId);
if (dir !== tmpNamed) { try { renameSync(dir, tmpNamed); dir = tmpNamed; } catch { /* validate in place using expectId */ } }
const manifest = validateManifest(parsed, dir, expectId);
if (!manifest) return { ok: false, problems: ['manifest failed validation (see ⚠️ above)'], manifest: null, dir };
const audit = auditPlugin(dir);
if (!audit.ok) return { ok: false, problems: audit.findings.map(f => `${f.file}: ${f.issue}`), manifest, dir };View on GitHub (pinned to aac998c7ed)