santifer/career-ops · error · Error
refusing non-GitHub/unsafe repo URL: ${arg} (expected https:
Error message
refusing non-GitHub/unsafe repo URL: ${arg} (expected https://github.com/<owner>/<repo>) What it means
Thrown by parseRepoArg() in plugin-install.mjs when a plugin source argument is not a safe GitHub URL. After normalizing owner/repo shorthand and stripping a trailing .git, the value must match the strict GITHUB_URL_RE (https://github.com/<owner>/<repo>). This blocks git protocol/file schemes, other hosts, and malformed inputs before any clone.
Source
Thrown at plugin-install.mjs:31
import { execFileSync } from 'node:child_process';
import { existsSync, mkdtempSync, mkdirSync, rmSync, cpSync, readdirSync, readFileSync, writeFileSync, renameSync } from 'node:fs';
import { tmpdir } from 'node:os';
import path from 'node:path';
import { validateManifest } from './plugins/_engine.mjs';
import { hashPluginTree } from './plugins/_lock.mjs';
import { auditPlugin } from './plugin-audit.mjs';
const GITHUB_URL_RE = /^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+?(?:\.git)?$/;
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;View on GitHub (pinned to 9b17a8ac97)
Solutions
- Use the HTTPS URL form: https://github.com/<owner>/career-ops-plugin-<name>.
- Or pass the owner/repo shorthand: <owner>/career-ops-plugin-<name>.
- Avoid SSH (git@github.com:...) and any non-github.com host.
- Re-copy the URL fresh from the GitHub repo page.
Example fix
// before
parseRepoArg('git@github.com:acme/career-ops-plugin-x.git');
// after
parseRepoArg('acme/career-ops-plugin-x'); Defensive patterns
Strategy: validation
Validate before calling
const GITHUB_URL_RE = /^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+?(?:\.git)?$/;
function isSafeGithubRepo(s) {
let url = s;
if (/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(s)) url = `https://github.com/${s}`;
url = url.replace(/\.git$/, '');
return GITHUB_URL_RE.test(url);
} Type guard
/** Narrows a string to a safe owner/repo or https github.com URL form. */
function isSafeGithubRepo(s) {
if (typeof s !== 'string') return false;
let url = s;
if (/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(s)) url = `https://github.com/${s}`;
url = url.replace(/\.git$/, '');
return /^https:\/\/github\.com\/[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(url);
} Prevention
- Always copy the HTTPS clone URL from GitHub, never the SSH one.
- Validate the URL with parseRepoArg() before wiring it into install automation.
- Only GitHub is supported; reject other hosts at the UX boundary.
When it happens
Trigger: Passing a git@github.com:... SSH URL; an https:// bitbucket/giteab/gitlab URL; a file:/// or git:// URL; a URL with a port, userinfo, query, or fragment that breaks the strict regex; a raw local path.
Common situations: User copy-pasted the SSH clone URL from GitHub instead of the HTTPS one; attempting to install a plugin hosted outside GitHub (not supported); a malformed clipboard copy introduced extra characters.
Related errors
- a 40-hex commit --sha is required (got ${JSON.stringify(sha)
- Refusing non-HTTP(S) URL: ${url}
- repo must be named "career-ops-plugin-<name>" (got "${repoNa
- clone of ${url}@${sha.slice(0, 10)} failed — ${err.stderr ?
- plugin rejected: - ${result.problems.join('\n - ')}
AI-assisted analysis of santifer/career-ops@9b17a8ac97 (2026-08-13).
Data as JSON: /api/errors/bc2b6fec859974a3.
Report an issue: GitHub.