@@ -68,12 +68,18 @@ func LoadUserConfig(v *venv.Venv, opts ...ConfigOption) (*Config, error) {
6868
6969 config := NewConfig (v .FS ).WithProviderInstallation (& ProviderInstallation {})
7070
71+ var helperSources []string
72+
7173 for _ , path := range paths {
7274 fileConfig , err := loadUserConfigFile (v , path )
7375 if err != nil {
7476 return nil , err
7577 }
7678
79+ if fileConfig .CredentialsHelpers != nil {
80+ helperSources = append (helperSources , path )
81+ }
82+
7783 mergeUserConfig (config , fileConfig )
7884 }
7985
@@ -82,7 +88,7 @@ func LoadUserConfig(v *venv.Venv, opts ...ConfigOption) (*Config, error) {
8288 config .PluginCacheDir = pluginCacheDir
8389 }
8490
85- if err := validateUserConfig (config ); err != nil {
91+ if err := validateUserConfig (config , helperSources ); err != nil {
8692 return nil , err
8793 }
8894
@@ -195,6 +201,15 @@ func loadUserConfigFile(v *venv.Venv, path string) (*Config, error) {
195201 return nil , fmt .Errorf ("%w: decoding %s: %w" , ErrUserConfig , path , err )
196202 }
197203
204+ // Ranging a map to pick "the" helper would decide by iteration order, so reject the
205+ // ambiguity the way OpenTofu's Config.Validate does.
206+ if len (file .CredentialsHelpers ) > 1 {
207+ return nil , fmt .Errorf (
208+ "%w: no more than one credentials_helper block may be specified, %s declares %d" ,
209+ ErrInvalidUserConfig , path , len (file .CredentialsHelpers ),
210+ )
211+ }
212+
198213 methods , err := decodeProviderInstallation (path , node )
199214 if err != nil {
200215 return nil , err
@@ -307,8 +322,16 @@ func expandUserConfigEnv(value string, env map[string]string) string {
307322}
308323
309324// validateUserConfig rejects the malformed blocks OpenTofu rejects, so a bad hostname surfaces
310- // here rather than as an unauthenticated registry request later.
311- func validateUserConfig (config * Config ) error {
325+ // here rather than as an unauthenticated registry request later. helperSources names every
326+ // file that declared a credentials_helper, which upstream allows only once across them all.
327+ func validateUserConfig (config * Config , helperSources []string ) error {
328+ if len (helperSources ) > 1 {
329+ return fmt .Errorf (
330+ "%w: no more than one credentials_helper block may be specified, found one in each of %s" ,
331+ ErrInvalidUserConfig , strings .Join (helperSources , ", " ),
332+ )
333+ }
334+
312335 for _ , creds := range config .Credentials {
313336 if _ , err := svchost .ForComparison (creds .Name ); err != nil {
314337 return fmt .Errorf (
@@ -373,7 +396,8 @@ func userCredentials(file userConfigFile) []ConfigCredentials {
373396 return credentials
374397}
375398
376- // userCredentialsHelper returns the single credentials_helper block, or nil when the file declares none.
399+ // userCredentialsHelper returns the single credentials_helper block, or nil when the file
400+ // declares none. More than one is rejected by the caller, so the map holds at most one entry.
377401func userCredentialsHelper (file userConfigFile ) * ConfigCredentialsHelper {
378402 for name , helper := range file .CredentialsHelpers {
379403 var args []string
0 commit comments