puppetlabs/puppet · error · Puppet::ParseError

Attempt to assign to a reserved variable name: '%{name}'

Error message

Attempt to assign to a reserved variable name: '%{name}'

What it means

Raised by Scope#setvar when assigning to the reserved names 'trusted' or 'facts' without options[:privileged]. These hashes are injected once by the compiler from node facts and certificate data; user code must not overwrite them because every consumer (manifests, functions, Hiera interpolation of $facts/$trusted) trusts their origin. Only internal calls marked :privileged may set them.

Source

Thrown at lib/puppet/parser/scope.rb:784

  TYPENAME_CLASS = 'Class'
  TYPENAME_NODE = 'Node'

  # Set a variable in the current scope.  This will override settings
  # in scopes above, but will not allow variables in the current scope
  # to be reassigned.
  #   It's preferred that you use self[]= instead of this; only use this
  # when you need to set options.
  def setvar(name, value, options = EMPTY_HASH)
    if name =~ /^[0-9]+$/
      raise Puppet::ParseError, _("Cannot assign to a numeric match result variable '$%{name}'") % { name: name } # unless options[:ephemeral]
    end
    unless name.is_a? String
      raise Puppet::ParseError, _("Scope variable name %{name} is a %{class_type}, not a string") % { name: name.inspect, class_type: name.class }
    end

    # Check for reserved variable names
    if (name == VARNAME_TRUSTED || name == VARNAME_FACTS) && !options[:privileged]
      raise Puppet::ParseError, _("Attempt to assign to a reserved variable name: '%{name}'") % { name: name }
    end

    # Check for server_facts reserved variable name
    if name == VARNAME_SERVER_FACTS && !options[:privileged]
      raise Puppet::ParseError, _("Attempt to assign to a reserved variable name: '%{name}'") % { name: name }
    end

    table = effective_symtable(options[:ephemeral])
    if table.bound?(name)
      error = Puppet::ParseError.new(_("Cannot reassign variable '$%{name}'") % { name: name })
      error.file = options[:file] if options[:file]
      error.line = options[:line] if options[:line]
      raise error
    end

    table[name] = value

    # Assign the qualified name in the environment

View on GitHub (pinned to e227c27540)

Solutions

  1. Read $facts/$trusted instead of assigning; derive a local variable: `$myfacts = $facts + { 'extra' => 'v' }` ( Puppet 4 hash merge via +).
  2. To add real facts, ship a custom fact (lib/facter/*.rb) so the value comes from the node itself.
  3. For test harnesses, inject facts at the node level (e.g. puppet apply --loadclasses-style node definitions, rspec-puppet's let(:facts)) rather than assigning in the manifest.
  4. If you truly are compiler-internal code, pass `privileged: true` to setvar.

Example fix

# before
$facts = { 'kernel' => 'Linux' }   # Attempt to assign to a reserved variable name: 'facts'

# after
$local_facts = $facts + { 'extra_info' => 'value' }
notify { $local_facts['kernel']: }
Defensive patterns

Strategy: validation

Validate before calling

# Before writing code, check the reserved list:
RESERVED = %w[trusted facts server_facts].freeze
raise ArgumentError, "reserved: #{n}" if RESERVED.include?(n)
# DSL: derive, don't assign:
$local = $facts + { 'extra' => 'v' }

Prevention

When it happens

Trigger: Manifest assignments `$facts = {...}`, `$facts['kernel'] = 'Linux'`, or `$trusted = {...}`. Ruby functions calling `scope.setvar('facts', hash)` or `scope['trusted'] = data` without `privileged: true` in the options hash. Also templates or defined types trying to 'extend' the facts hash by reassigning the whole variable.

Common situations: Modules written for very old Puppet (pre-3.5) where $facts did not exist and users emulated it; attempts to fake facts in tests or local apply runs; copying ENC output into $trusted during provisioning scripts.

Related errors


AI-assisted analysis of puppetlabs/puppet@e227c27540 (2026-08-21). Data as JSON: /api/errors/561fe7f2457c8e3e. Report an issue: GitHub.