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

  1. Read the wrapped cause and the arg index to find the offending argument.
  2. Export the referenced environment variable (e.g. export API_KEY=...) before launching crush, or put it in your shell profile.
  3. Define the value via crush's secrets/env config instead of a bare {{env.X}} reference.
  4. 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

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


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