odysseus-dev/odysseus · error · SkillImportError
Invalid raw GitHub URL
Error message
Invalid raw GitHub URL
What it means
Raised by parse_skill_source (services/memory/skill_importer.py) when a raw.githubusercontent.com URL has fewer than 4 non-empty path segments. Raw URLs must follow /owner/repo/ref/path/to/file — the importer maps segments 1-3 to owner/repo/ref and the rest to the in-repo path, so a URL like https://raw.githubusercontent.com/owner/repo or .../owner/repo/main cannot identify a file and is rejected as malformed.
Source
Thrown at services/memory/skill_importer.py:319
raise SkillImportError(
"skills.sh did not redirect to GitHub — open the skill's "
"repository on GitHub, navigate to the exact skill folder or "
"SKILL.md file, and paste that URL; the repository-root link "
"alone is not sufficient"
)
url = final
# Update parsed and hostname to reflect the new GitHub URL
parsed = urlparse(url)
hostname = (parsed.hostname or "").lower()
_assert_github_url(url)
if hostname == "raw.githubusercontent.com":
# /owner/repo/ref/path/to/file
bits = [p for p in parsed.path.split("/") if p]
if len(bits) < 4:
raise SkillImportError("Invalid raw GitHub URL")
owner, repo, ref = bits[0], bits[1], bits[2]
path = "/".join(bits[3:])
return ResolvedSource(owner=owner, repo=repo, ref=ref, path=path)
bits = [p for p in parsed.path.split("/") if p]
if len(bits) < 2:
raise SkillImportError("Invalid GitHub URL")
owner, repo = bits[0], bits[1]
ref = "main"
path = ""
if len(bits) >= 4 and bits[2] in ("tree", "blob"):
ref = bits[3]
path = "/".join(bits[4:])
elif len(bits) == 2:
path = ""
else:
raise SkillImportError("GitHub URL must include /tree/<branch>/... or /blob/<branch>/...")View on GitHub (pinned to f9235ebbf1)
Solutions
- Use a complete raw URL including ref and file path: https://raw.githubusercontent.com/<owner>/<repo>/<ref>/skills/<name>/SKILL.md.
- Get the URL the reliable way: open the file on github.com, click the Raw button, copy the address bar.
- If the branch contains slashes, that is fine — segment 3 onward is treated as ref+path only when counts line up; prefer the tree/blob web URL form to avoid ambiguity.
- Verify the URL resolves: curl -I <raw url> should return 200.
Example fix
# before
import_skill("https://raw.githubusercontent.com/owner/repo")
SkillImportError: Invalid raw GitHub URL
# after
import_skill("https://raw.githubusercontent.com/owner/repo/main/skills/foo/SKILL.md") Defensive patterns
Strategy: validation
Validate before calling
from urllib.parse import urlparse
def parse_raw_url(url: str) -> tuple[str, str, str, str]:
bits = [p for p in urlparse(url).path.split('/') if p]
if urlparse(url).hostname != 'raw.githubusercontent.com' or len(bits) < 4:
raise ValueError('raw URL must be https://raw.githubusercontent.com/<owner>/<repo>/<ref>/<path>')
return bits[0], bits[1], bits[2], '/'.join(bits[3:]) Type guard
def is_valid_raw_github_url(url: str) -> bool:
try:
parse_raw_url(url)
return True
except ValueError:
return False Try / catch
try:
src = parse_skill_source(raw_url)
except SkillImportError as e:
if str(e) == 'Invalid raw GitHub URL':
return bad_request('Raw URLs need owner/repo/branch/file, e.g. .../raw.githubusercontent.com/o/r/main/SKILL.md')
raise Prevention
- Get raw URLs via the Raw button on github.com rather than typing them by hand.
- Validate the 4-segment shape before submission.
- curl -I the URL to confirm it returns 200 before import.
When it happens
Trigger: Pasting https://raw.githubusercontent.com/<owner>/<repo> (repo root), .../<owner>/<repo>/<branch> (branch with no file), or a URL whose path collapsed to empty (trailing garbage after the host); hand-constructing a raw URL and forgetting the file component.
Common situations: Users trimming the URL while sharing; copying the raw base from docs examples and appending only owner/repo; branch names with slashes making the segment count look right but the intended file missing.
Related errors
- {context} must stay on GitHub (got {host or 'unknown host'})
- skills.sh did not redirect to GitHub — open the skill's repo
- Invalid GitHub URL
- d && d.detail ? d.detail : ('HTTP ' + res.status)
- Server did not return a session id
AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14).
Data as JSON: /api/errors/212affe91f9dd1a2.
Report an issue: GitHub.