charmbracelet/crush · error
arg %d: %w
Error message
arg %d: %w
What it means
MCPConfig.ResolvedArgs resolves each entry of the MCP server's Args list through VariableResolver.ResolveValue, which expands template variables (e.g. {{env.VAR}}) in config values. If resolving any single argument fails, it is wrapped as `arg <index>: <cause>`. Typical causes are references to undefined environment variables or secrets that cannot be found.
Source
Thrown at internal/config/config.go:458
}
// ResolvedArgs returns m.Args with every element expanded through the
// given resolver. A fresh slice is allocated; m.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.
//
// See ResolvedEnv for guidance on picking a resolver.
func (m MCPConfig) ResolvedArgs(r VariableResolver) ([]string, error) {
if len(m.Args) == 0 {
return nil, nil
}
out := make([]string, len(m.Args))
for i, a := range m.Args {
v, err := r.ResolveValue(a)
if err != nil {
return nil, fmt.Errorf("arg %d: %w", i, err)
}
out[i] = v
}
return out, nil
}
// ResolvedURL returns m.URL expanded through the given resolver. The
// receiver is not mutated. Errors from the resolver are already
// sanitized by ResolveValue and are wrapped with %w for errors.Is/As.
//
// URLs run through the same shell-expansion pipeline as the other
// fields, so a literal '$' (e.g. OData query strings containing
// $filter/$select) must be escaped as '\$' or '${DOLLAR:-$}' to avoid
// being interpreted as a variable reference. Same constraint already
// applies to command, args, env, and headers.
//
// See ResolvedEnv for guidance on picking a resolver.
func (m MCPConfig) ResolvedURL(r VariableResolver) (string, error) {View on GitHub (pinned to 7944b8e522)
Solutions
- Read the wrapped cause and the arg index to find the offending argument.
- Export the referenced environment variable (e.g. export API_KEY=...) before launching crush, or put it in your shell profile.
- Define the value via crush's secrets/env config instead of a bare {{env.X}} reference.
- Fix typos in the {{...}} variable name in the MCP args in crushrc.
Example fix
// before (crushrc)
mcp myserver --command npx --args '-y','pkg','--key','{{env.MY_API_KEY}}' // MY_API_KEY unset
// after
export MY_API_KEY=sk-... # then rerun crush Defensive patterns
Strategy: validation
Validate before calling
# verify every {{env.X}} referenced in MCP args exists 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
args, err := m.ResolvedArgs(resolver)
if err != nil {
var miss *MissingEnvError // example typed cause from the resolver
if errors.As(err, &miss) {
return fmt.Errorf("set %s before starting MCP server", miss.Name)
}
return err
} Prevention
- Export all env vars referenced by MCP args in your shell profile.
- Keep env var names consistent between crushrc and CI secrets.
- Prefer crush's secrets configuration over ad-hoc {{env.X}} where possible.
- Dry-run config loading (start crush) after editing MCP args.
When it happens
Trigger: Starting or resolving an MCP server configured in crushrc whose `args` contain an unresolvable template variable — most commonly `{{env.SOME_VAR}}` where SOME_VAR is unset, or an invalid variable reference syntax.
Common situations: Defining an MCP stdio server whose args reference an env var that exists in one shell but not the one crush runs in; CI environments missing a secret; typo in the variable key inside {{...}}.
Related errors
- url: %w
- header %s: %w
- invalid mcp command: %w
- oauth_client_id: %w
- mcp stdio config requires a non-empty 'command' field
AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29).
Data as JSON: /api/errors/9d72695520a39b58.
Report an issue: GitHub.