odysseus-dev/odysseus · error · SkillImportError
{context} must stay on GitHub (got {host or 'unknown host'})
Error message
{context} must stay on GitHub (got {host or 'unknown host'}) What it means
Raised by _assert_github_url in services/memory/skill_importer.py when a URL used during skill import is not hosted on one of the allowlisted GitHub hosts (github.com, www.github.com, api.github.com, raw.githubusercontent.com). The importer is SSRF-hardened: every hop must stay on GitHub, and any other hostname (or no hostname at all) is rejected with the offending host included in the message.
Source
Thrown at services/memory/skill_importer.py:40
ALLOWED_SUFFIXES = (
".md", ".txt", ".json", ".yaml", ".yml", ".py", ".sh", ".toml",
".js", ".ts", ".css", ".html", ".xml", ".csv",
)
TEXT_NAMES = {"skill.md", "license", "license.md", "readme.md"}
_GITHUB_HOSTS = frozenset({
"github.com", "www.github.com", "api.github.com", "raw.githubusercontent.com",
})
_SKILLS_SH_HOSTS = frozenset({"skills.sh", "www.skills.sh"})
def _github_host(url: str) -> str:
return (urlparse(str(url)).hostname or "").lower()
def _assert_github_url(url: str, *, context: str = "URL") -> None:
host = _github_host(url)
if host not in _GITHUB_HOSTS:
raise SkillImportError(
f"{context} must stay on GitHub (got {host or 'unknown host'})"
)
@dataclass
class ResolvedSource:
owner: str
repo: str
ref: str
path: str # directory or file path inside repo (no leading slash)
class SkillImportError(ValueError):
pass
def _safe_relpath(rel: str) -> str:
rel = (rel or "").replace("\\", "/").strip().lstrip("/")View on GitHub (pinned to f9235ebbf1)
Solutions
- Re-import using a canonical URL on a supported host: https://github.com/<owner>/<repo>/tree/<ref>/<skill-path>, https://raw.githubusercontent.com/..., or https://skills.sh/...
- Inspect the exact host in the error message — 'unknown host' means the URL string is malformed, so re-copy it without whitespace/quotes.
- If the URL came from a redirect, fetch it manually (curl -I) and check each Location hop; the importer requires every hop on GitHub.
- If you genuinely need a non-GitHub source, that is unsupported by design — vendor the skill into a GitHub repo first.
Example fix
# before
import_skill("https://gitlab.com/foo/bar-skill")
SkillImportError: URL must stay on GitHub (got gitlab.com)
# after
import_skill("https://github.com/foo/bar/tree/main/skills/bar") Defensive patterns
Strategy: validation
Validate before calling
from urllib.parse import urlparse
_GITHUB_HOSTS = {'github.com', 'www.github.com', 'api.github.com', 'raw.githubusercontent.com'}
def assert_github(url: str) -> None:
host = (urlparse(str(url)).hostname or '').lower()
if host not in _GITHUB_HOSTS:
raise ValueError(f'not a GitHub URL: {host or "(no host — malformed URL)"}') Type guard
def is_github_url(url: str) -> bool:
return (urlparse(str(url)).hostname or '').lower() in _GITHUB_HOSTS Try / catch
try:
src = parse_skill_source(url)
except SkillImportError as e:
if 'must stay on GitHub' in str(e):
# show the user the supported URL formats instead of retrying
raise UserFacingError('Paste a github.com or skills.sh URL, e.g. https://github.com/owner/repo/tree/main/skills/name') from e
raise Prevention
- In the import UI, validate the hostname client-side before submission.
- Trim/strip pasted URLs and reject empty hosts early so users see 'malformed URL' rather than 'unknown host'.
- When following redirects in your own fetchers, re-assert the allowlist on every hop, as this importer does.
When it happens
Trigger: A skills.sh redirect chain that lands on a non-GitHub host; a SKILL.md or bundle URL pointing at gitlab.com, a personal CDN, or an attacker-controlled domain; a malformed URL (e.g. 'github.com owner/repo' with a space, or 'not a url') whose urlparse hostname is empty, yielding 'unknown host'; a redirect Location header with a relative/host-less target at a point where the raw value is validated.
Common situations: Users pasting a GitLab or generic git host link into skill import; skills.sh changing its redirect target; redirect chains that pass through an intermediate tracker domain; URLs pasted with leading whitespace or line breaks that break hostname parsing.
Related errors
- outbound URL blocked: {reason}
- unsupported URL scheme: {scheme}
- skills.sh did not redirect to GitHub — open the skill's repo
- Invalid raw GitHub URL
- Invalid GitHub URL
AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14).
Data as JSON: /api/errors/7874eb7561c34b17.
Report an issue: GitHub.