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