charmbracelet/crush · error

failed to read skill

Error message

failed to read skill: %w

What it means

This error is returned by Client.ReadSkill when the HTTP POST request to /workspaces/{id}/skills/read fails at the transport level, before any HTTP status could be evaluated. Like the other client errors, the underlying cause is wrapped with %w so callers can inspect it. It means the round trip to read the skill content never completed.

Solutions

  1. Confirm the server is reachable and the base URL is correct.
  2. Inspect the wrapped cause with errors.Is/As to distinguish timeout vs connection refused.
  3. Extend the context timeout if reading large skills triggers timeouts.
  4. Retry the POST once with backoff for transient network faults.
  5. Check proxy/firewall rules allow POST to this endpoint.

Example fix

// before
c, err := client.ReadSkill(ctx, wsID, skillID)
if err != nil { return err }
// after
c, err := client.ReadSkill(ctx, wsID, skillID)
if err != nil {
    if errors.Is(err, context.Canceled) {
        return fmt.Errorf("skill read cancelled by caller: %w", err)
    }
    return fmt.Errorf("could not reach server to read skill: %w", err)
}
Defensive patterns

Strategy: try-catch

Validate before calling

// Ensure a deadline exists before the call
if _, ok := ctx.Deadline(); !ok {
    var cancel context.CancelFunc
    ctx, cancel = context.WithTimeout(ctx, 15*time.Second)
    defer cancel()
}

Type guard

func isTransportError(err error) bool {
    var ne net.Error
    return errors.As(err, &ne) || errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded)
}

Try / catch

skill, err := client.ReadSkill(ctx, wsID, skillID)
if err != nil {
    if isTransportError(err) {
        return retryWithBackoff(func() error {
            _, err = client.ReadSkill(ctx, wsID, skillID)
            return err
        })
    }
    return err
}

Prevention

When it happens

Trigger: Calling ReadSkill(ctx, workspaceID, skillID) when the connection is refused/reset, DNS resolution fails, the request times out, or the context is cancelled before the response arrives.

Common situations: Server down or restarted between calls; misconfigured base URL; corporate proxy blocking POST requests; context deadline from a short-lived request scope.

Related errors


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

Appendix: source

Thrown at internal/client/config.go:242

	}
	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 {
		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"`

View on GitHub (pinned to 7944b8e522)