{"record":{"id":"516b7a9cf7602fd2","repo":"santifer/career-ops","slug":"a-40-hex-commit-sha-is-required-got-json-stri","errorCode":null,"errorMessage":"a 40-hex commit --sha is required (got ${JSON.stringify(sha)})","messagePattern":"a 40-hex commit --sha is required \\(got (.+?)\\)","errorType":"validation","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"plugin-install.mjs","lineNumber":40,"sourceCode":"const NAME_RE = /^career-ops-plugin-([a-z0-9][a-z0-9-]*)$/;\nconst SHA_RE = /^[0-9a-f]{40}$/;\nconst MIN_FILES = ['manifest.json', 'index.mjs', 'README.md', 'LICENSE'];\n\n/** Normalize `owner/repo` | full URL into a validated github URL + the plugin id. */\nexport function parseRepoArg(arg) {\n  let url = arg;\n  if (/^[A-Za-z0-9_.-]+\\/[A-Za-z0-9_.-]+$/.test(arg)) url = `https://github.com/${arg}`;\n  url = url.replace(/\\.git$/, '');\n  if (!GITHUB_URL_RE.test(url)) throw new Error(`refusing non-GitHub/unsafe repo URL: ${arg} (expected https://github.com/<owner>/<repo>)`);\n  const repoName = url.split('/').pop() || '';\n  const m = NAME_RE.exec(repoName);\n  if (!m) throw new Error(`repo must be named \"career-ops-plugin-<name>\" (got \"${repoName}\")`);\n  return { url, id: m[1] };\n}\n\n/** Clone the EXACT pinned SHA into a fresh temp dir. Returns the temp dir path. */\nexport function safeClone(url, sha) {\n  if (!SHA_RE.test(sha || '')) throw new Error(`a 40-hex commit --sha is required (got ${JSON.stringify(sha)})`);\n  const dir = mkdtempSync(path.join(tmpdir(), 'co-plugin-'));\n  const git = (...args) => execFileSync('git', ['-c', 'protocol.ext.allow=never', '-c', 'protocol.file.allow=never', ...args], { stdio: ['ignore', 'ignore', 'pipe'], timeout: 120_000 });\n  try {\n    git('-C', dir, 'init', '-q');\n    git('-C', dir, 'remote', 'add', 'origin', '--', url);\n    git('-C', dir, 'fetch', '--depth', '1', '--no-tags', '-q', 'origin', sha);\n    git('-C', dir, 'checkout', '-q', 'FETCH_HEAD');\n    rmSync(path.join(dir, '.git'), { recursive: true, force: true }); // drop VCS metadata (and any hooks)\n    return dir;\n  } catch (err) {\n    rmSync(dir, { recursive: true, force: true });\n    throw new Error(`clone of ${url}@${sha.slice(0, 10)} failed — ${err.stderr ? String(err.stderr).slice(0, 200) : err.message}`);\n  }\n}\n\n/** Check the minimum file set + a valid manifest whose id matches `expectId`. */\nexport function validateInstall(dir, expectId) {\n  const problems = [];","sourceCodeStart":22,"sourceCodeEnd":58,"githubUrl":"https://github.com/santifer/career-ops/blob/9b17a8ac97b398a496b38e423ae24e433b43254f/plugin-install.mjs#L22-L58","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before\nsafeClone(url, 'abc1234');\n// after\nsafeClone(url, 'abc1234567890abcdef1234567890abcdef1234');","handlingStrategy":"validation","validationCode":"const SHA_RE = /^[0-9a-f]{40}$/;\nfunction isValidCommitSha(s) {\n  return SHA_RE.test(s || '');\n}\n// resolve a short sha to a full one before install:\n// git ls-remote https://github.com/<o>/<r> <short>`","typeGuard":"/** Narrows a string to a full 40-hex git commit SHA. */\nfunction isFullCommitSha(s) {\n  return typeof s === 'string' && /^[0-9a-f]{40}$/.test(s);\n}","tryCatchPattern":null,"preventionTips":["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."],"tags":["plugins","git","sha-pinning","security","supply-chain"],"backgroundTag":null,"analyzedSha":"9b17a8ac97b398a496b38e423ae24e433b43254f","analyzedAt":"2026-08-13T00:48:39.135Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}