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
- Read the wrapped cause and header name to identify the failing entry.
- Export the referenced environment variable before starting crush.
- Move the secret into crush's secrets/env configuration so it resolves reliably.
- 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
- Export auth secrets (tokens) before launching crush in any environment.
- Mirror required secrets into CI/CD environments explicitly.
- Prefer empty-header skipping semantics: leave a header unset rather than templating an optional secret.
- Review header templates after secret rotation.
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.