charmbracelet/crush · error

header %s: %w

Error message

header %s: %w

What it means

MCPConfig.ResolvedHeaders resolves each MCP server header value through VariableResolver.ResolveValue (iterating keys in sorted order), wrapping failures as `header <name>: <cause>`. Empty resolved values are skipped. This lets headers like Authorization source their values from env/secrets via {{env.TOKEN}} style references.

Source

Thrown at internal/config/config.go:543

// header" is clearly "absent when the var isn't set."
//
// See ResolvedEnv for guidance on picking a resolver.
func (m MCPConfig) ResolvedHeaders(r VariableResolver) (map[string]string, error) {
	if len(m.Headers) == 0 {
		return map[string]string{}, nil
	}
	out := make(map[string]string, len(m.Headers))
	// Sort keys so failures are reported deterministically when more
	// than one header would fail.
	keys := make([]string, 0, len(m.Headers))
	for k := range m.Headers {
		keys = append(keys, k)
	}
	slices.Sort(keys)
	for _, k := range keys {
		v, err := r.ResolveValue(m.Headers[k])
		if err != nil {
			return nil, fmt.Errorf("header %s: %w", k, err)
		}
		if v == "" {
			continue
		}
		out[k] = v
	}
	return out, nil
}

// ResolvedArgs returns l.Args with every element expanded through the
// given resolver. A fresh slice is allocated; l.Args is never mutated.
// On the first resolution failure it returns nil and an error
// identifying the offending positional index; the inner resolver error
// is already sanitized by ResolveValue and is wrapped with %w so
// errors.Is/As continues to work.
//
// Empty resolved values are kept (a deliberate "empty positional arg"
// like --flag "" is sometimes valid), matching MCPConfig.ResolvedArgs.

View on GitHub (pinned to 7944b8e522)

Solutions

  1. Read the wrapped cause and header name to identify the failing entry.
  2. Export the referenced environment variable before starting crush.
  3. Move the secret into crush's secrets/env configuration so it resolves reliably.
  4. Fix the {{...}} variable spelling in the header value in crushrc.

Example fix

// before (crushrc)
mcp acme --url 'https://acme/mcp' --header 'Authorization=Bearer {{env.ACME_TOKEN}}' // ACME_TOKEN unset
// after
export ACME_TOKEN=...  # then rerun crush
Defensive patterns

Strategy: validation

Validate before calling

# ensure env vars used in MCP headers are set before starting crush
grep -o '{{env\.[A-Z_]*}}' crushrc | sed 's/{{env\.//;s/}}//' | sort -u | while read -r v; do
  [ -n "${!v+x}" ] || echo "missing env var: $v"
done

Try / catch

headers, err := m.ResolvedHeaders(resolver)
if err != nil {
    return fmt.Errorf("cannot resolve MCP headers: %w", err)
}

Prevention

When it happens

Trigger: Resolving an MCP config whose `headers` map contains a value with an unresolvable variable — e.g. "Authorization": "Bearer {{env.MCP_TOKEN}}" where MCP_TOKEN is unset or the reference is malformed.

Common situations: Auth headers templated from secrets present locally but absent in CI/production; rotating a secret and removing the env var; typo in the variable key inside the header value.

Related errors


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