aaif-goose/goose · error

Failed to parse GitHub API response: {}

Error message

Failed to parse GitHub API response: {}

What it means

goose parses the stdout of `gh api repos/<repo>/contents` as JSON (crates/goose-cli/src/recipes/github_recipe.rs:270-272). serde_json::from_slice failed, meaning gh exited zero but its stdout was not a valid JSON document — gh debug output, warnings, or a proxy-injected page got mixed into stdout instead of the expected API payload.

Source

Thrown at crates/goose-cli/src/recipes/github_recipe.rs:271

    use std::process::Command;

    // Ensure GitHub CLI is authenticated
    ensure_gh_authenticated()?;

    // Get repository contents using GitHub CLI
    let output = Command::new("gh")
        .args(["api", &format!("repos/{}/contents", repo)])
        .set_no_window()
        .output()
        .map_err(|e| anyhow!("Failed to fetch repository contents using 'gh api' command (executed when GOOSE_RECIPE_GITHUB_REPO is configured). This requires GitHub CLI (gh) to be installed and authenticated. Error: {}", e))?;

    if !output.status.success() {
        let error_msg = String::from_utf8_lossy(&output.stderr);
        return Err(anyhow!("GitHub API request failed: {}", error_msg));
    }

    let contents: Value = serde_json::from_slice(&output.stdout)
        .map_err(|e| anyhow!("Failed to parse GitHub API response: {}", e))?;

    let mut recipes = Vec::new();

    if let Some(items) = contents.as_array() {
        for item in items {
            if let (Some(name), Some(item_type)) = (
                item.get("name").and_then(|n| n.as_str()),
                item.get("type").and_then(|t| t.as_str()),
            ) {
                if item_type == "dir" {
                    // Check if this directory contains a recipe file
                    if let Ok(recipe_info) = check_github_directory_for_recipe(repo, name) {
                        recipes.push(recipe_info);
                    }
                }
            }
        }
    }

View on GitHub (pinned to 3810898a74)

Solutions

  1. Run `gh api repos/<owner>/<repo>/contents | head` and inspect what precedes/breaks the JSON; unset GH_DEBUG and GH_DEBUG_API for the goose invocation
  2. Upgrade or pin a known-good gh version if its stdout format changed
  3. Remove any proxy/wrapper that decorates stdout for gh commands

Example fix

# before
export GH_DEBUG=api
export GOOSE_RECIPE_GITHUB_REPO=owner/repo
goose recipe list

# after
unset GH_DEBUG GH_DEBUG_API
export GOOSE_RECIPE_GITHUB_REPO=owner/repo
goose recipe list
Defensive patterns

Strategy: validation

Validate before calling

# Confirm gh emits pure JSON in this environment
gh api "repos/$GOOSE_RECIPE_GITHUB_REPO/contents" | python3 -c 'import json,sys; json.load(sys.stdin); print("valid JSON")'

Try / catch

// Rust: on parse failure, include a snippet of the offending stdout
let contents: Value = match serde_json::from_slice(&output.stdout) {
    Ok(v) => v,
    Err(e) => anyhow::bail!(
        "non-JSON gh output ({e}): {}",
        String::from_utf8_lossy(&output.stdout[..output.stdout.len().min(200)])
    ),
};

Prevention

When it happens

Trigger: GH_DEBUG/GH_DEBUG_API set makes gh print request logs to stdout alongside the body; a wrapper/proxy rewrites the response; gh prints a banner or non-JSON notice (update prompts) to stdout; or truncated output from a killed process.

Common situations: Debug env vars left exported in the shell, gh version changes that emit notices, terminals/pagers interfering when output is captured, or HTTPS proxies returning HTML error pages that gh passes through.

Understand the failure class

Related errors


AI-assisted analysis of aaif-goose/goose@3810898a74 (2026-08-16). Data as JSON: /api/errors/30da4c2f37f798eb. Report an issue: GitHub.