diff --git a/internal/config/resolve.go b/internal/config/resolve.go index 8c22a8abc7a516cd19be6fc32eaa101d60e416d4..8e13e16d440a31b73672ab55a467cc7ff866a1b8 100644 --- a/internal/config/resolve.go +++ b/internal/config/resolve.go @@ -58,8 +58,11 @@ type shellVariableResolver struct { // NewShellVariableResolver returns a VariableResolver that delegates to // the embedded shell (the same interpreter used by the bash tool and // hooks). Supported constructs match shell.ExpandValue: $VAR, ${VAR}, -// ${VAR:-default}, $(command), quoting, and escapes. Unset variables are -// an error; use ${VAR:-} to opt in to an empty fallback. +// ${VAR:-default}, $(command), quoting, and escapes. Unset variables +// expand to the empty string by default, matching bash; use +// ${VAR:?message} to require a value and fail loudly when it is missing. +// The stricter "unset is always an error" mode is gated globally by +// shell.NoUnset. func NewShellVariableResolver(e env.Env, opts ...ShellResolverOption) VariableResolver { r := &shellVariableResolver{ env: e, @@ -77,9 +80,12 @@ func NewShellVariableResolver(e env.Env, opts ...ShellResolverOption) VariableRe // - $VAR and ${VAR} for environment variables. // - ${VAR:-default} / ${VAR:+alt} / ${VAR:?msg} for defaulting. // -// Unset variables are a hard error (nounset), mirroring the historical -// behaviour of this resolver: silently expanding an unset variable to the -// empty string is exactly how broken credentials reach MCP servers. +// Unset variables expand to the empty string by default, matching bash. +// Command-substitution failures are always a hard error. Required +// credentials should use ${VAR:?message} so a missing variable fails +// loudly at load time instead of quietly resolving to empty. Global +// strict mode is available via shell.NoUnset for callers that want the +// old nounset-on behaviour back. func (r *shellVariableResolver) ResolveValue(value string) (string, error) { // Preserve the historical backward-compat contract: a lone "$" is a // malformed config value, not a legal literal. The underlying shell