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

  1. 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/...
  2. Inspect the exact host in the error message — 'unknown host' means the URL string is malformed, so re-copy it without whitespace/quotes.
  3. If the URL came from a redirect, fetch it manually (curl -I) and check each Location hop; the importer requires every hop on GitHub.
  4. 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

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


AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14). Data as JSON: /api/errors/7874eb7561c34b17. Report an issue: GitHub.