charmbracelet/crush · error

failed to decode skill response: %w

Error message

failed to decode skill response: %w

What it means

Returned by Client.ReadSkill when the server returned 200 OK but the body failed to decode into proto.ReadSkillResponse. Typical causes are an empty body, malformed JSON, or a payload whose fields don't match the proto.ReadSkillResponse struct. It signals a broken response contract rather than a request problem.

Source

Thrown at internal/client/config.go:250

	}
	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 {
	rsp, err := c.post(ctx, fmt.Sprintf("/workspaces/%s/mcp/docker/enable", id), nil, nil, nil)
	if err != nil {
		return fmt.Errorf("failed to enable docker MCP: %w", err)
	}

View on GitHub (pinned to 7944b8e522)

Solutions

  1. Capture the raw body via server logs or a proxy to see the actual 200 payload.
  2. Compare the payload against proto.ReadSkillResponse's json tags and update the mismatched side.
  3. Upgrade client or server so both agree on the read-skills response schema.
  4. Check for body-corrupting middleware (compression, charset, truncation).
  5. Reproduce with curl -X POST .../skills/read and validate the JSON with jq.

Example fix

// before
r, err := client.ReadSkill(ctx, wsID, skillID) // decode error on 200
// after
// Verify shape: curl -s -X POST $BASE/workspaces/$WS/skills/read -d '{"skill_id":"..."}' | jq
r, err := client.ReadSkill(ctx, wsID, skillID)
if err != nil {
    return fmt.Errorf("unexpected read-skill response; check server version: %w", err)
}
Defensive patterns

Strategy: type-guard

Type guard

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

func validReadSkillResponse(r *proto.ReadSkillResponse) bool {
    return r != nil && r.Content != ""
}

Try / catch

skill, err := client.ReadSkill(ctx, wsID, skillID)
if err != nil {
    if isDecodeError(err) {
        // contract mismatch on 200 body; fall back or surface version skew
        return fmt.Errorf("server response incompatible: %w", err)
    }
    return err
}
if !validReadSkillResponse(skill) {
    return ErrMalformedSkillPayload
}

Prevention

When it happens

Trigger: Server handler for /workspaces/{id}/skills/read returns 200 with an error JSON object, null body, truncated output, or renamed fields inconsistent with proto.ReadSkillResponse.

Common situations: Server version drift after an API change; middleware or compression misconfiguration corrupting the body; a skill whose content contains bytes that break encoding on the server side.

Understand the failure class

Related errors


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