santifer/career-ops · error · Error
a 40-hex commit --sha is required
Error message
a 40-hex commit --sha is required (got ${JSON.stringify(sha)}) What it means
safeClone fetches exactly one pinned commit; the sha must be a full 40-character hex SHA-1 so the exact revision is unambiguous and un-forgeable. Anything else — short sha, branch/tag name, empty or non-string value — is refused with this message before any git command runs.
Solutions
- Get the full sha: `git ls-remote https://github.com/owner/career-ops-plugin-<name> <ref>` or the 'Copy full SHA for permalink' button on GitHub
- Pass the complete 40-hex string via --sha on the CLI
- Fix scripts that truncate or prefix the sha; never pass branch or tag names
- If pinning is awkward, record the full sha once at review time and store it in your install config
Example fix
// before node plugins.mjs install acme/career-ops-plugin-demo --sha a1b2c3d // after node plugins.mjs install acme/career-ops-plugin-demo --sha a1b2c3d4e5f6... (full 40 hex chars)
Defensive patterns
Strategy: validation
Validate before calling
if (!/^[0-9a-f]{40}$/.test(sha)) throw new Error(`--sha must be a full 40-hex commit SHA, got: ${sha}`); Type guard
const isFullSha = (s) => typeof s === 'string' && /^[0-9a-f]{40}$/.test(s); Try / catch
try { const dir = safeClone(url, sha); } catch (e) { if (e.message.includes('40-hex commit --sha is required')) { console.error('Resolve the full sha: git ls-remote <url> <ref>'); } else throw e; } Prevention
- Always pin the full 40-hex sha from git ls-remote or GitHub's copy-full-sha button
- Never pass branch or tag names as sha
- Require --sha explicitly in scripts; fail fast when it is undefined
- Store the pinned sha in a reviewed config rather than deriving it at runtime
When it happens
Trigger: safeClone(url, sha) called with a short sha (7-10 chars from GitHub UI), a tag/branch name like 'main', an empty string, undefined/null, or a 64-hex sha256 (future git object format).
Common situations: User copies the short sha from the GitHub commit list; passes the default branch instead of a commit; forgets --sha on the CLI so it arrives undefined; a script stores the sha of a subdirectory commit log line including decoration text.
Understand the failure class
Background: "Invalid ... format", "must be in format X", "does not look like a ..." — invalid argument format errors across CLI tools and libraries — this error's family across 17 libraries.
Related errors
- arbeitnow: URL must use HTTPS
- eightfold: URL must use HTTPS
- justjoin: untrusted hostname
- local-parser: careers_url must be http(s)
- local-parser: company name cannot start with
AI-assisted analysis of santifer/career-ops@aac998c7ed (2026-09-16).
Data as JSON: /api/errors/516b7a9cf7602fd2.
Report an issue: GitHub.
Appendix: source
Thrown at plugin-install.mjs:42
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 aac998c7ed)