hashicorp/terraform · error

credentials file %s has invalid value for "credentials" prop

Error message

credentials file %s has invalid value for "credentials" property: must be a JSON object

What it means

Emitted by `updateLocalHostCredentials` (credentials.go:356-358) when the top-level `"credentials"` property of `credentials.tfrc.json` parses as JSON but is not a JSON object — e.g. it is a string, number, or array. Terraform expects `{ "credentials": { "<host>": { ... } } }`; any other shape for the `credentials` key is rejected before any host is written.

Source

Thrown at internal/command/cliconfig/credentials.go:358

		// json.Number and thus avoid losing any accuracy in our round-trip.
		dec := json.NewDecoder(bytes.NewReader(oldSrc))
		dec.UseNumber()
		err = dec.Decode(&raw)
		if err != nil {
			return fmt.Errorf("cannot read %s: %s", filename, err)
		}
	} else {
		raw = make(map[string]interface{})
	}

	rawCredsI, ok := raw["credentials"]
	if !ok {
		rawCredsI = make(map[string]interface{})
		raw["credentials"] = rawCredsI
	}
	rawCredsMap, ok := rawCredsI.(map[string]interface{})
	if !ok {
		return fmt.Errorf("credentials file %s has invalid value for \"credentials\" property: must be a JSON object", filename)
	}

	// We use display-oriented hostnames in our file to mimick how a human user
	// would write it, so we need to search for and remove any key that
	// normalizes to our target hostname so we won't generate something invalid
	// when the existing entry is slightly different.
	for givenHost := range rawCredsMap {
		canonHost, err := svchost.ForComparison(givenHost)
		if err == nil && canonHost == host {
			delete(rawCredsMap, givenHost)
		}
	}

	// If we have a new object to store we'll write it in now. If the previous
	// object had the hostname written in a different way then this will
	// appear to change it into our canonical display form, with all the
	// letters in lowercase and other transforms from the Internationalized
	// Domain Names specification.

View on GitHub (pinned to c9def3e214)

Solutions

  1. Rewrite the `credentials` value as an object keyed by hostname: `{ "credentials": { "app.terraform.io": { "token": "..." } } }`.
  2. Validate the shape with `jq '.credentials | type'` — it must print `object`.
  3. If unsure of contents, back up the file and let `terraform login` recreate it correctly.

Example fix

// before
{ "credentials": "atlasv1.something" }
// credentials file ... has invalid value for "credentials" property: must be a JSON object

// after
{
  "credentials": {
    "app.terraform.io": { "token": "atlasv1.something" }
  }
}
Defensive patterns

Strategy: type-guard

Validate before calling

// Ensure the 'credentials' property is a JSON object before Terraform touches it.
func credsPropertyIsObject(path string) error {
    b, err := os.ReadFile(path)
    if err != nil { return nil } // absent file is fine
    var v map[string]interface{}
    if err := json.Unmarshal(b, &v); err != nil { return nil } // handled by 557
    c, ok := v["credentials"]
    if !ok { return nil }
    if _, ok := c.(map[string]interface{}); !ok {
        return fmt.Errorf("'credentials' must be a JSON object, got %T", c)
    }
    return nil
}

Type guard

// Type guard for the credentials property after decode.
func isCredentialsObject(raw map[string]interface{}) bool {
    c, ok := raw["credentials"]
    if !ok { return true } // absent is valid
    _, isObj := c.(map[string]interface{})
    return isObj
}

Prevention

When it happens

Trigger: Storing/forgetting credentials when the file's `credentials` field is a scalar or array, e.g. `{ "credentials": "atlasv1..." }` or `{ "credentials": [ ... ] }`.

Common situations: Hand-editing the file and flattening the structure; an older/incorrect tool writing tokens directly under `credentials`; schema drift from a non-Terraform tool that shares the file.

Related errors


AI-assisted analysis of hashicorp/terraform@c9def3e214 (2026-08-07). Data as JSON: /api/errors/88cd2c47f5e87ebe. Report an issue: GitHub.