hashicorp/terraform · error

invalid null string in 'scripts'

Error message

invalid null string in 'scripts'

What it means

Thrown by the remote-exec provisioner's generateScripts (resource_provisioner.go:158). generateScripts iterates the `inline` argument as a value slice; each element must be a concrete string. If any element is null (l.IsNull()), the provisioner cannot build a shell script and returns this error before any SSH connection is made.

Solutions

  1. Filter null entries out of the `inline` list with a compact expression, e.g. `inline = [for s in var.cmds : s if s != null]`.
  2. Give every list element a concrete string value or a non-null default.

Example fix

# before
variable "cmds" { type = list(string) }
provisioner "remote-exec" {
  inline = var.cmds
}
# after
variable "cmds" { type = list(string) }
provisioner "remote-exec" {
  inline = [for s in var.cmds : s if s != null]
}
Defensive patterns

Strategy: validation

Validate before calling

// Reject null entries before passing inline to remote-exec.
func cleanInline(lines []any) ([]string, error) {
    out := make([]string, 0, len(lines))
    for _, l := range lines {
        if l == nil { return nil, errors.New("invalid null string in 'scripts'") }
        out = append(out, l.(string))
    }
    return out, nil
}

Type guard

func inlineHasNoNulls(v cty.Value) bool {
    if v.IsNull() { return true }
    for _, e := range v.AsValueSlice() {
        if e.IsNull() { return false }
    }
    return true
}

Prevention

When it happens

Trigger: A provisioner "remote-exec" block with `inline = [var.maybe_null, "echo done"]` where one list element evaluates to null at apply time, causing generateScripts to bail.

Common situations: inline list built from variables/locals where one entry is conditionally null. for/for_each building the inline list that yields a null element.

Related errors


AI-assisted analysis of hashicorp/terraform@d32a084675 (2026-08-11). Data as JSON: /api/errors/5e8866169084871e. Report an issue: GitHub.

Appendix: source

Thrown at internal/builtin/provisioners/remote-exec/resource_provisioner.go:158

	return resp
}

func (p *provisioner) Stop() error {
	p.cancel()
	return nil
}

func (p *provisioner) Close() error {
	return nil
}

// generateScripts takes the configuration and creates a script from each inline config
func generateScripts(inline cty.Value) ([]string, error) {
	var lines []string
	for _, l := range inline.AsValueSlice() {
		if l.IsNull() {
			return nil, errors.New("invalid null string in 'scripts'")
		}

		s := l.AsString()
		if s == "" {
			return nil, errors.New("invalid empty string in 'scripts'")
		}
		lines = append(lines, s)
	}
	lines = append(lines, "")

	return []string{strings.Join(lines, "\n")}, nil
}

// collectScripts is used to collect all the scripts we need
// to execute in preparation for copying them.
func collectScripts(v cty.Value) ([]io.ReadCloser, error) {
	// Check if inline
	if inline := v.GetAttr("inline"); !inline.IsNull() {

View on GitHub (pinned to d32a084675)