jdx/mise · error

repository is outside the approved relay scope

Error message

repository is outside the approved relay scope

What it means

After extracting OWNER/REPO from the path, `authorize` checks it against the relay's approved scope via `scope.permits(&name)` (lowercased). If the requested repository is not in the allowlist configured for this relay session, the request is refused, even if the path shape and operation would otherwise be allowed. The relay is deliberately scoped so credentials can only reach specific repositories.

Solutions

  1. Add the requested repository to the relay's approved scope (restart the relay with the additional OWNER/REPO in the scope flag)
  2. Correct the client to use the scoped repository's exact owner/repo spelling
  3. If a tool is following redirects to other repos, pin it to the original repository or disable the redirecting behavior

Example fix

// before
mise github-relay --scope "octocat/hello-world"
git clone https://relay/git/octocat/hello-world-2.git
// after
mise github-relay --scope "octocat/hello-world"
git clone https://relay/git/octocat/hello-world.git
Defensive patterns

Strategy: validation

Validate before calling

fn scope_permits(scope_repos: &[String], url_repo: &str) -> bool {
    scope_repos.iter().any(|r| r.to_ascii_lowercase() == url_repo.to_ascii_lowercase())
}
// call before issuing the request
assert!(scope_permits(&["octocat/hello-world".into()], "octocat/hello-world"));

Try / catch

match authorize(&scope, method, path, query) {
    Err(e) if e.to_string().contains("outside the approved relay scope") => {
        eprintln!("repo not in scope; restart relay with it added to --scope");
    }
    Ok(t) => forward(t),
    Err(e) => return Err(e),
}

Prevention

When it happens

Trigger: A client requests `api/repos/other/repo`, `git/other/repo`, or `web/other/repo/...` where `other/repo` was not included in the scope passed at relay startup (e.g. scope was `octocat/hello-world` but the clone URL points at a fork under another owner).

Common situations: Cloning a fork whose owner differs from the scoped repo; a tool following a redirect or submodule URL to a different repository; a typo in either the scope flag or the client URL; case differences are fine (name is lowercased) but owner/repo spelling must match the scope.

Understand the failure class

Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.

Related errors


AI-assisted analysis of jdx/mise@533346cc37 (2026-09-17). Data as JSON: /api/errors/e3c6ccfdda809dc5. Report an issue: GitHub.

Appendix: source

Thrown at src/github_relay.rs:206

        {
            bail!("invalid relay path");
        }
    }
    Ok(())
}

#[cfg(any(unix, test))]
fn authorize(scope: &Scope, method: &str, path: &str, query: Option<&str>) -> Result<Target> {
    validate_path(path)?;
    let p: Vec<_> = path.split('/').collect();
    let (owner, repo) = match p.as_slice() {
        ["api", "repos", owner, repo, ..] => (*owner, *repo),
        ["git" | "web", owner, repo, ..] => (*owner, repo.strip_suffix(".git").unwrap_or(repo)),
        _ => bail!("unsupported GitHub operation"),
    };
    let name = repository(&format!("{owner}/{repo}"))?;
    if !scope.permits(&name) {
        bail!("repository is outside the approved relay scope");
    }
    let git = p[0] == "git";
    let allowed = match p.as_slice() {
        ["git", _, _, "info", "refs"] => {
            method == "GET" && query == Some("service=git-upload-pack")
        }
        ["git", _, _, "git-upload-pack"] => method == "POST" && query.is_none(),
        ["api", "repos", _, _] => method == "GET" || method == "HEAD",
        ["api", "repos", _, _, "git", kind, ..] => {
            matches!(*kind, "refs" | "matching-refs") && matches!(method, "GET" | "HEAD")
        }
        ["api", "repos", _, _, kind, ..] => {
            matches!(
                *kind,
                "contents" | "releases" | "tags" | "branches" | "tarball" | "zipball"
            ) && matches!(method, "GET" | "HEAD")
        }
        ["web", _, _, "releases", "download", _, ..] => matches!(method, "GET" | "HEAD"),

View on GitHub (pinned to 533346cc37)