charmbracelet/crush · error

failed to decode skills: %w

Error message

failed to decode skills: %w

What it means

Returned by Client.ListSkills when the server responded 200 OK but the response body could not be decoded into []proto.SkillInfo. The server sent malformed JSON, an unexpected shape (e.g. an object wrapping the array, or an error payload with a 200 status), or an empty/truncated body. This is a client/server contract mismatch.

Source

Thrown at internal/client/config.go:231

	if err := json.NewDecoder(rsp.Body).Decode(&result); err != nil {
		return "", fmt.Errorf("failed to decode init prompt response: %w", err)
	}
	return result.Prompt, nil
}

// ListSkills retrieves the visible skills for a workspace.
func (c *Client) ListSkills(ctx context.Context, id string) ([]proto.SkillInfo, error) {
	rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/skills", id), nil, nil)
	if err != nil {
		return nil, fmt.Errorf("failed to list skills: %w", err)
	}
	defer rsp.Body.Close()
	if rsp.StatusCode != http.StatusOK {
		return nil, fmt.Errorf("failed to list skills: status code %d", rsp.StatusCode)
	}
	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 {

View on GitHub (pinned to 7944b8e522)

Solutions

  1. Dump the raw response body (e.g. with a debug HTTP proxy or by checking server logs) to see what was actually returned.
  2. Verify the server's skills endpoint returns a JSON array matching proto.SkillInfo's json tags.
  3. Check for client/server version skew and upgrade the mismatched side.
  4. Bypass any proxy/middleware to confirm it isn't rewriting the response.
  5. Validate the payload with curl | jq to confirm it is a well-formed array.

Example fix

// before
skills, err := client.ListSkills(ctx, wsID) // fails: cannot unmarshal object into []SkillInfo
// after
// Confirm server shape first:
//   curl -s $BASE/workspaces/$WS/skills | jq 'type'  -> must be "array"
skills, err := client.ListSkills(ctx, wsID)
if err != nil {
    return fmt.Errorf("server returned unexpected skills payload; check client/server versions: %w", err)
}
Defensive patterns

Strategy: type-guard

Type guard

func isDecodeError(err error) bool {
    return err != nil && strings.Contains(err.Error(), "failed to decode skills")
}

func isValidSkillList(skills []proto.SkillInfo) bool {
    for _, s := range skills { if s.ID == "" { return false } }
    return true
}

Try / catch

skills, err := client.ListSkills(ctx, wsID)
if err != nil {
    if isDecodeError(err) {
        // contract mismatch: log raw payload via server logs, degrade gracefully
        return emptySkillsFallback()
    }
    return err
}

Prevention

When it happens

Trigger: Server returns 200 with HTML (proxy error page), an empty body, a JSON object instead of an array, or field types that don't match proto.SkillInfo's JSON schema.

Common situations: Reverse proxy intercepting the request and returning a 200 HTML page; server-side API version change renaming or retyping SkillInfo fields; middleware writing extra content to the response body.

Understand the failure class

Related errors


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