santifer/career-ops · error · Error
a 40-hex commit --sha is required (got ${JSON.stringify(sha)
Error message
a 40-hex commit --sha is required (got ${JSON.stringify(sha)}) What it means
Thrown by safeClone() in plugin-install.mjs when the --sha argument is missing or does not match SHA_RE (exactly 40 lowercase hex characters). Plugins must be installed at a pinned immutable commit SHA so the reviewed code is exactly what runs — no floating tags/branches that can be force-updated.
Source
Thrown at plugin-install.mjs:40
const NAME_RE = /^career-ops-plugin-([a-z0-9][a-z0-9-]*)$/;
const SHA_RE = /^[0-9a-f]{40}$/;
const MIN_FILES = ['manifest.json', 'index.mjs', 'README.md', 'LICENSE'];
/** Normalize `owner/repo` | full URL into a validated github URL + the plugin id. */
export function parseRepoArg(arg) {
let url = arg;
if (/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(arg)) url = `https://github.com/${arg}`;
url = url.replace(/\.git$/, '');
if (!GITHUB_URL_RE.test(url)) throw new Error(`refusing non-GitHub/unsafe repo URL: ${arg} (expected https://github.com/<owner>/<repo>)`);
const repoName = url.split('/').pop() || '';
const m = NAME_RE.exec(repoName);
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 = [];View on GitHub (pinned to 9b17a8ac97)
Solutions
- Get the full 40-char commit SHA from GitHub (Commits → copy full SHA, or git rev-parse <sha>^{commit}).
- Pass it as --sha <40-hex> to the install command.
- Do not use short SHAs, tags, or branch names.
- If you only have a short SHA, resolve it locally first: git ls-remote then git rev-parse.
Example fix
// before safeClone(url, 'abc1234'); // after safeClone(url, 'abc1234567890abcdef1234567890abcdef1234');
Defensive patterns
Strategy: validation
Validate before calling
const SHA_RE = /^[0-9a-f]{40}$/;
function isValidCommitSha(s) {
return SHA_RE.test(s || '');
}
// resolve a short sha to a full one before install:
// git ls-remote https://github.com/<o>/<r> <short>` Type guard
/** Narrows a string to a full 40-hex git commit SHA. */
function isFullCommitSha(s) {
return typeof s === 'string' && /^[0-9a-f]{40}$/.test(s);
} Prevention
- Always pin to a full 40-char commit SHA, never a tag or branch.
- Copy the full SHA from GitHub's commits list, not the UI's abbreviated form.
- Reject short SHAs at the CLI/UX boundary with a helpful message.
When it happens
Trigger: Omitting --sha entirely; passing a short SHA (e.g. abc1234); passing a branch/tag name like 'main' or 'v1.0'; passing an uppercase or 64-char SHA; extra whitespace in the value.
Common situations: User expected to install from latest/main and did not supply a SHA; copy-pasted a 7-char abbreviated SHA from GitHub's UI; passed a tag thinking it was a commit.
Related errors
- refusing non-GitHub/unsafe repo URL: ${arg} (expected https:
- clone of ${url}@${sha.slice(0, 10)} failed — ${err.stderr ?
- repo must be named "career-ops-plugin-<name>" (got "${repoNa
- plugin rejected: - ${result.problems.join('\n - ')}
- plugin egress must use HTTPS: ${u.href}
AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13).
Data as JSON: /api/errors/516b7a9cf7602fd2.
Report an issue: GitHub.