charmbracelet/crush · error

failed to decode init prompt response: %w

Error message

failed to decode init prompt response: %w

What it means

After a 200 response, GetInitializePrompt decodes the body expecting {"prompt": string}. Any decode failure (empty body, non-JSON, wrong shape) raises this error. It signals a server/client response-contract mismatch, not a connectivity issue, and propagates to InitializePrompt.

Source

Thrown at internal/client/config.go:214

	return nil
}

// GetInitializePrompt retrieves the initialization prompt from the
// server.
func (c *Client) GetInitializePrompt(ctx context.Context, id string) (string, error) {
	rsp, err := c.get(ctx, fmt.Sprintf("/workspaces/%s/project/init-prompt", id), nil, nil)
	if err != nil {
		return "", fmt.Errorf("failed to get init prompt: %w", err)
	}
	defer rsp.Body.Close()
	if rsp.StatusCode != http.StatusOK {
		return "", fmt.Errorf("failed to get init prompt: status code %d", rsp.StatusCode)
	}
	var result struct {
		Prompt string `json:"prompt"`
	}
	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)
	}

View on GitHub (pinned to 7944b8e522)

Solutions

  1. Log or capture the raw response body to see the actual payload
  2. Align client and server versions for the init-prompt contract
  3. Reconfigure or bypass proxies/SSO walls that return HTML with 200
  4. Add a server-side guard so internal errors never return 200 with a non-JSON body

Example fix

// before
prompt, err := client.GetInitializePrompt(ctx, id)
if err != nil {
    return err
}
// after
prompt, err := client.GetInitializePrompt(ctx, id)
if err != nil {
    log.Printf("init-prompt decode failed; check server version/proxy: %v", err)
    return err
}
Defensive patterns

Strategy: type-guard

Validate before calling

// Verify the endpoint returns JSON before decoding
testResp, err := http.Get(client.BaseURL() + "/healthz")
if err != nil {
    return err
}
ct := testResp.Header.Get("Content-Type")
testResp.Body.Close()
if !strings.HasPrefix(ct, "application/json") {
    return fmt.Errorf("non-JSON API responses detected (proxy/auth wall?)")
}

Type guard

func looksLikeJSON(b []byte) bool {
    t := bytes.TrimSpace(b)
    return len(t) > 0 && (t[0] == '{' || t[0] == '[')
}

Try / catch

prompt, err := c.GetInitializePrompt(ctx, id)
if err != nil {
    if strings.Contains(err.Error(), "failed to decode") {
        // contract mismatch: fall back to a local default prompt
        return defaultInitPrompt, nil
    }
    return err
}

Prevention

When it happens

Trigger: Calling GetInitializePrompt when the 200 body is not decodable into {prompt: string}: empty body, HTML injected by a proxy or auth wall, or a server version returning a renamed/nested prompt field.

Common situations: Client/server version skew after the init-prompt payload changed; reverse proxy or SSO login page returned instead of JSON; compression middleware misconfiguration; server returning 200 with an empty body on internal errors.

Understand the failure class

Related errors


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