charmbracelet/crush · error

failed to read skill: status code

Error message

failed to read skill: status code %d

What it means

Returned by Client.ReadSkill when the POST completed but the server replied with a non-200 status code. The library requires 200 with a proto.ReadSkillResponse body; 404 (skill or workspace not found), 401/403 (auth), or 500 all produce this error. The numeric status is embedded in the message because the body is discarded.

Solutions

  1. Read the status code from the error message; for 404, re-list skills to confirm the skillID is still valid.
  2. For 401/403, re-authenticate or verify workspace permissions.
  3. For 500, check server logs for the skills/read handler failure.
  4. Always fetch skill IDs from a fresh ListSkills call rather than a stale cache.
  5. Verify the workspace ID in the URL path is correct.

Example fix

// before
skill, err := client.ReadSkill(ctx, wsID, skillID) // status code 404
// after
skills, _ := client.ListSkills(ctx, wsID)
if !containsSkill(skills, skillID) {
    return fmt.Errorf("skill %s no longer exists", skillID)
}
skill, err := client.ReadSkill(ctx, wsID, skillID)
Defensive patterns

Strategy: validation

Validate before calling

// Confirm the skill still exists before reading
skills, err := client.ListSkills(ctx, wsID)
if err != nil { return err }
found := false
for _, s := range skills {
    if s.ID == skillID { found = true; break }
}
if !found { return fmt.Errorf("skill %s not found", skillID) }

Type guard

func isNon200(err error) (code int, ok bool) {
    m := regexp.MustCompile(`status code (\d+)`).FindStringSubmatch(err.Error())
    if len(m) < 2 { return 0, false }
    code, err2 := strconv.Atoi(m[1])
    return code, err2 == nil
}

Try / catch

skill, err := client.ReadSkill(ctx, wsID, skillID)
if err != nil {
    if code, ok := isNon200(err); ok && code == 404 {
        return ErrSkillNotFound // handle gracefully
    }
    return err
}

Prevention

When it happens

Trigger: Calling ReadSkill with a skillID that no longer exists (404), with insufficient permissions (403), an invalid workspace ID (404), or during a server-side failure (500).

Common situations: Skill deleted or renamed by another agent/user before the read; listing skills, then reading from a stale cached list; expired session token; server bug returning 500 for large skill files.

Related errors


AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29). Data as JSON: /api/errors/d351192ed58beee6. Report an issue: GitHub.

Appendix: source

Thrown at internal/client/config.go:246

	}
	var skills []proto.SkillInfo
	if err := json.NewDecoder(rsp.Body).Decode(&skills); err != nil {
		return nil, fmt.Errorf("failed to decode skills: %w", err)
	}
	return skills, nil
}

// ReadSkill reads a skill's content by ID from the server.
func (c *Client) ReadSkill(ctx context.Context, id, skillID string) (*proto.ReadSkillResponse, error) {
	rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/skills/read", id), nil, jsonBody(proto.ReadSkillRequest{
		SkillID: skillID,
	}), http.Header{"Content-Type": []string{"application/json"}})
	if err != nil {
		return nil, fmt.Errorf("failed to read skill: %w", err)
	}
	defer rsp.Body.Close()
	if rsp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("failed to read skill: status code %d", rsp.StatusCode)
	}
	var result proto.ReadSkillResponse
	if err := json.NewDecoder(rsp.Body).Decode(&result); err != nil {
		return nil, fmt.Errorf("failed to decode skill response: %w", err)
	}
	return &result, nil
}

// MCPResourceContents holds the contents of an MCP resource.
type MCPResourceContents struct {
	URI      string `json:"uri"`
	MIMEType string `json:"mime_type,omitempty"`
	Text     string `json:"text,omitempty"`
	Blob     []byte `json:"blob,omitempty"`
}

// EnableDockerMCP enables the Docker MCP server on the workspace.
func (c *Client) EnableDockerMCP(ctx context.Context, id string) error {

View on GitHub (pinned to 7944b8e522)