charmbracelet/crush · error

failed to read skill: status code %d

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.

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)

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.