Hmbown/CodeWhale · error

failed to download skill (last status: {})

Error message

failed to download skill (last status: {})

What it means

After trying every candidate URL, download_first_success bails with the last non-404 HTTP status (or 'unknown' if none was recorded) when no bytes were obtained and no host was denied or left pending approval. It is the aggregate 'all downloads failed' error. 404 is deliberately treated as 'try the next candidate', so this fires on non-404 failures such as 403 or 5xx.

Source

Thrown at crates/tui/src/skills/install.rs:1129

            DownloadAttempt::Bytes(bytes) => {
                return Ok(DownloadOutcome::Bytes {
                    bytes,
                    url: url.clone(),
                });
            }
            DownloadAttempt::NotFound(status) => {
                last_status = Some(status);
                continue;
            }
        }
    }
    if let Some(host) = denied_host {
        return Ok(DownloadOutcome::Denied(host));
    }
    if let Some(host) = prompt_host {
        return Ok(DownloadOutcome::NeedsApproval(host));
    }
    bail!(
        "failed to download skill (last status: {})",
        last_status
            .map(|s| s.to_string())
            .unwrap_or_else(|| "unknown".to_string())
    );
}

enum DownloadAttempt {
    Bytes(Vec<u8>),
    NotFound(reqwest::StatusCode),
}

/// Stream a URL into memory with a size cap. Aborts on the first read that
/// would push the buffer over `max_size * 4` (the *4 accounts for compression;
/// the unpack step still enforces `max_size` on the *uncompressed* bytes).
async fn download_with_cap(url: &str, max_size: u64) -> Result<DownloadAttempt> {
    let resp = reqwest_client()
        .get(url)

View on GitHub (pinned to 0c42157ee5)

Solutions

  1. Retry after a short wait; transient 5xx responses and rate limits clear on their own.
  2. Act on the status: 403 points to rate limiting or a private repo (check auth); 5xx points to upstream trouble.
  3. Verify the repo's default branch and pass a DirectUrl to that branch's exact archive URL.
  4. If rate-limited, spread installs out or authenticate the requests.

Example fix

# before
/skill install github:owner/repo
# -> failed to download skill (last status: 403 Forbidden)

# after: point at the exact default-branch archive
/skill install https://github.com/owner/repo/archive/refs/heads/trunk.tar.gz
Defensive patterns

Strategy: retry

Try / catch

const MAX_TRIES: u32 = 3;
for attempt in 1..=MAX_TRIES {
    match skills::install::install(&spec, &dir, &network).await {
        Err(err) if err.to_string().starts_with("failed to download skill") && attempt < MAX_TRIES => {
            tokio::time::sleep(std::time::Duration::from_secs(2u64.pow(attempt))).await;
        }
        other => return other.map(|_| ()),
    }
}

Prevention

When it happens

Trigger: Both GitHub archive candidates (main, then master) fail with 5xx/403 for a repo, or a direct URL host errors out, while the network policy allows the hosts. Because a non-404 error status aborts the loop immediately via '?', the reported 'last status' is the first hard failure.

Common situations: GitHub rate limiting (403) during bursts, private repos without credentials, artifact host outages, and transient proxy failures.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@0c42157ee5 (2026-08-20). Data as JSON: /api/errors/d7c0e159ac7dacb4. Report an issue: GitHub.