jdx/mise · error
GitHub relay permits read-only repository operations only
Error message
GitHub relay permits read-only repository operations only
What it means
The relay allowlists only read-only operations per path: git smart-HTTP upload-pack handshake and POST, GET/HEAD on repo metadata, refs, contents, releases, tags, branches, tarballs/zipballs, and web release/archive downloads. Any request whose method or path shape falls outside these patterns is rejected because the relay must never forward credentials to mutating or arbitrary GitHub operations.
Solutions
- Restrict the client to read-only operations (clone/fetch, GET/HEAD API reads, release and archive downloads)
- Perform write operations (push, API mutations) directly against github.com with your own credentials instead of via the relay
- Check the endpoint kind is one of: info/refs, git-upload-pack, repo metadata, git/refs|matching-refs, contents, releases, tags, branches, tarball, zipball, releases/download, archive
- Use GET or HEAD (except the git-upload-pack POST) — switch any mutating method
Example fix
// before POST https://relay/git/org/repo/info/refs?service=git-receive-pack // after git push https://github.com/org/repo.git # writes bypass the read-only relay
Defensive patterns
Strategy: validation
Validate before calling
const READ_ONLY: [(&str, [&str; 2]); 2] = [("GET", []), ("HEAD", [])];
fn is_read_only(method: &str, is_git_upload_pack_post: bool) -> bool {
method == "GET" || method == "HEAD" || is_git_upload_pack_post
}
assert!(is_read_only("GET", false)); Try / catch
match authorize(&scope, method, path, query) {
Err(e) if e.to_string().contains("read-only repository operations") => {
eprintln!("{method} {path} is not permitted; perform writes directly on github.com");
}
Ok(t) => forward(t),
Err(e) => return Err(e),
} Prevention
- Never point write workflows (push, API mutations) at the relay
- Restrict relayed clients to clone/fetch and GET/HEAD reads
- Check endpoint kind against the allowlist before issuing requests
- Configure git remotes so push goes directly to github.com while fetch uses the relay
When it happens
Trigger: A `POST` to `git/OWNER/REPO/info/refs` (only GET is allowed); a git push attempt (`POST git-upload-pack` is allowed, but `git-receive-pack` paths are not in the allowlist, so pushes fail); `DELETE`/`PATCH`/`PUT` on `api/repos/...`; `POST` to any `web/` download path; any unrecognized path under an allowed prefix (e.g. `api/repos/o/r/commits`).
Common situations: Trying to `git push` through the relay (receive-pack is not permitted); a tool issuing write API calls (create release, edit file) through the proxy; a client using an endpoint kind not in the allowlist like `commits`, `pulls`, or `issues`.
Related errors
- repository is outside the approved relay scope
- unsupported archive format
- unsupported GitHub operation
- unsupported query parameter
- expected a GitHub repository in OWNER/REPO form
AI-assisted analysis of jdx/mise@533346cc37 (2026-09-17).
Data as JSON: /api/errors/332a4b41aa6b3a55.
Report an issue: GitHub.
Appendix: source
Thrown at src/github_relay.rs:229
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"),
["web", _, _, "archive", _, ..] => matches!(method, "GET" | "HEAD"),
_ => false,
};
if !allowed {
bail!("GitHub relay permits read-only repository operations only");
}
if !git && let Some(query) = query {
for (key, _) in url::form_urlencoded::parse(query.as_bytes()) {
if !matches!(key.as_ref(), "ref" | "page" | "per_page") {
bail!("unsupported query parameter");
}
}
}
let host = if p[0] == "api" {
"api.github.com"
} else {
"github.com"
};
let suffix = path.split_once('/').expect("validated path").1;
let mut url = format!("https://{host}/{suffix}");
let archive_repo = match p.as_slice() {
["api", "repos", _, _, "tarball" | "zipball", ..] => Some(name.clone()),
["web", _, _, "archive", rest @ ..] => {View on GitHub (pinned to 533346cc37)