Files
IntuneManagement/Docs/Settings.md
T

13 KiB

Settings

Settings are the tool's configuration: everything the Settings dialog shows, plus a handful of hidden keys and per-feature state. This page is about how they are stored, how they are addressed, and how to drive them from automation without touching the machine they run on.

For cache and logging, see Settings Cache Logging.

Every registered key, with type, default and description, is in SettingsReference.md - generated from the code, so it is always current.

Three layers

Addressing a setting used to mean knowing where it lives. It does not any more:

Layer Where Addressed by Functions
storage Internal/Core.ps1 path (SubPath + key) Get-SettingStoreValue, Save-SettingStoreValue, Remove-SettingStoreValue
resolver Internal/Settings.ps1 key Get-SettingValue, Set-SettingValue, Remove-SettingValue, Test-SettingValueConfigured, Resolve-SettingValue
public Public/*Setting*.ps1 key Get-IMSetting, Set-IMSetting, Remove-IMSetting, Get-IMSettingDefinition, plus the store cmdlets below

The dependency direction is one-way: public calls the resolver, the resolver calls storage. Nothing goes the other way.

Prefer the resolver. It looks the key up in the registered definitions and derives the SubPath from the registration, so a write cannot land somewhere a read never looks. Every SubPath bug the project has had - GraphPageSize resolving differently in headless sessions, the bulk-export remember-last-used round trip being silently dead, DefaultCloud - was a hand-written path on one side only. Tools/Audit-Settings.ps1 reports remaining path-addressed access to a registered key as a warning.

Get-SettingValue lives in Internal/Core.ps1 rather than with the rest of the resolver because Write-Log reads LogFile through it during preload, long before Internal/ is dot-sourced.

Registration

A setting exists because some file called Add-SettingsObject:

Add-SettingsObject -Key "GraphPageSize" -Section "IntuneManager" -SubPath "IntuneManager" `
                   -Title "Page size" -Type "List" -DefaultValue "0" -ItemsSource $sizes

-SubPath is the storage path. With no -SubPath the -Section is used, except General, which stores at the root of the store. -Type is a UI editor hint (Boolean, Int, File, List, Text), not a storage type - everything on disk is a string.

Get-IMSettingDefinition lists the registrations; Get-IMSettingDefinition -Section IntuneManager narrows to one section.

Three store modes

The mode is decided at the very top of Internal/Core.ps1, before anything can log:

Mode Backing Default when
Registry HKCU:\Software\IntuneManagement on Windows
Json IM_SETTINGS_FILE, else LocalApplicationData/IntuneManagement/Settings.json off Windows, or IM_SETTINGS_FILE is set
Memory a tree with no file behind it - reads and writes work, nothing touches disk never; opt in explicitly

Environment variables, read once at load:

Variable Effect
IM_SETTINGS_STORE Memory, Json or Registry. Registry off Windows falls back to Json. An unrecognized value falls back to the platform default and is warned about once logging works.
IM_SETTINGS_FILE the Json store's file. Created if missing. Implies Json.

Memory mode is not a fourth code path: the storage primitives take their JSON branch whenever a settings object exists, and persist only when a settings file exists too. Memory mode is an object with no file.

Get-IMSettingsStore reports what the store actually is - which is not always what was requested, because a Json store whose file cannot be read falls back to the registry, and off Windows that leaves no store at all (Mode = "None": reads return registered defaults, writes go nowhere).

Get-IMSettingsStore                 # Mode / Path / Persisted / ValueCount / RequestedMode
Get-IMSettingsStore -IncludeValues   # ... plus every SubPath/Key/Value row

Public API

Get-IMSetting GraphPageSize                     # the effective value
Get-IMSetting GraphPageSize -Detailed           # value + Source (Default/Global/Tenant) + Type + Default + ...
Get-IMSetting ExportFolder -Scope Global        # ignore any tenant override
Get-IMSetting ExportFolder -Scope Tenant -TenantID <guid>

Set-IMSetting GraphPageSize 999
Set-IMSetting ExportFolder '\\server\intune\contoso' -Scope Tenant
Set-IMSetting UseBatchAPI $false -PassThru      # returns the resolved value afterwards

Remove-IMSetting ExportFolder -Scope Tenant     # revert to the global value
Remove-IMSetting ExportFolder                   # revert to the registered default

Set-IMSetting and Remove-IMSetting persist by default - to the registry or the settings file, whichever the active store is. There is no opt-in switch. They support -WhatIf, they log the store and path they wrote to, and Get-IMSettingsStore lets a script assert on the store before writing anything. If a script must not write to the machine it runs on, switch to a memory store first.

Get-IMSetting accepts a key from the pipeline, so Get-IMSettingDefinition -Section IntuneManager | Get-IMSetting -Detailed dumps a whole section with provenance.

Scope and precedence

A setting can be written globally or for one tenant. Reads take the tenant value first:

tenant value  ->  global value  ->  registered DefaultValue

-Scope Tenant with no -TenantID uses the connected tenant, and fails loudly if there is none - writing the global value instead would change every tenant when the caller asked for one.

Get-IMSetting -Detailed reports which level answered in Source. That is computed fresh from the store on every call; the .Value cached on a definition object by Get-SettingValue is a per-session artifact of whoever read it last.

A scoped read (-Scope Global / -Scope Tenant) reports the absence of a value at that level as $null with Source = 'NotSet'. It does not fall back to the registered default, because "this tenant does not override the setting" and "this tenant overrides it to the same value as the default" are different facts. Only the default -Scope Effective falls back, which is what the application itself resolves.

Keys with no registration

The hidden keys (ExportReplaceTokens) and the per-feature state namespaces are not registered with Add-SettingsObject, so they have no path to take from a registration. -SubPath supplies it, and is accepted on all three cmdlets - a key that can be written this way can be read and removed the same way:

Set-IMSetting    ExportReplaceTokens 'TenantId' -SubPath 'IntuneManager'
Get-IMSetting    ExportReplaceTokens -SubPath 'IntuneManager'
Remove-IMSetting ExportReplaceTokens -SubPath 'IntuneManager'

Such a key has no registered default and no declared type, so its Source is Tenant, Global or NotSet - never Default - and the value comes back as the string the store holds. Tenant precedence works as usual (-Scope Tenant, -TenantID), which is how a per-tenant ExportReplaceTokens is set.

For a registered key -SubPath is reported in the log and ignored: the registration decides the path, so a write cannot be aimed somewhere the application never reads.

Automation: a run that reads and writes nothing on the worker

The problem: a runbook on a shared Hybrid Worker has no business reading, let alone writing, that worker's HKCU hive or settings file. A memory store solves it, and a checked-in settings file makes the configuration reviewable:

Import-Module .\IntuneManagement.psd1

# Nothing from here on touches the worker's registry or settings file.
Use-IMSettingsStore -Memory
Import-IMSettingsStore .\config\documentation-run.json

# Whatever the file did not cover.
Set-IMSetting GraphPageSize 999
Set-IMSetting UseBatchAPI $true

Connect-IMIntuneManagement -Provider OAuth -TenantId $tid -AppId $appId -Certificate $cert
Start-IMGraphBulkDocumentation -OutputFormat html -PolicyGroup DeviceConfiguration

IM_SETTINGS_STORE=Memory in the environment does the same thing from the first line of the module load, which matters if anything the module logs during load would otherwise resolve against the real store.

Cmdlet Use
Use-IMSettingsStore -Memory [-Seed] switch to memory. -Seed copies the persisted values in first, so the session starts from the real configuration and then diverges without writing back.
Use-IMSettingsStore <path> switch to a Json store at that file, creating it if missing.
Use-IMSettingsStore -Registry switch back to HKCU. Windows only.
Export-IMSettingsStore <path> write the whole current store to a JSON file, whatever mode it is in - including a registry store.
Import-IMSettingsStore <path> load a settings file into the active store, value by value.

Two things about Import-IMSettingsStore worth knowing:

  • It merges into what is already there rather than replacing it. For a clean slate, Use-IMSettingsStore -Memory (without -Seed) first, then import into that.
  • It goes through the storage layer, so one implementation is correct for all three modes: values persist in Json mode, land in the registry in Registry mode, stay in memory in Memory mode. Which also means importing into a registry store writes to the registry.
  • Keys with no registration are imported anyway - the hidden keys and per-feature state namespaces are real - but they are listed in a warning, because a typo looks identical and would otherwise silently do nothing.

Stored shape

Everything is a string. Booleans are PascalCase "True"/"False", which is what $true.ToString() produces and what every existing store contains. Set-SettingValue coerces through Format-SettingStoreValue, so $true, "true" and "TRUE" all land in the one canonical shape, and a value written through the resolver is indistinguishable from one written by the Settings dialog.

Watch out for [bool]"False" - it is $true in PowerShell, as any non-empty string is. Stored Booleans are compared to "true", never cast. Both the reader and the writer do this the same way on purpose.

An empty string counts as "not set" on read, so clearing a text box in the Settings dialog restores the registered default rather than persisting "". "Is it configured" is therefore a different question from "does it have a value", and Test-SettingValueConfigured (Get-IMSetting does not expose it) answers it by asking whether the value exists at that path at all.

Legacy layout note

A SubPath with more than one level (<tenantid>\IntuneManager) used to be stored by the JSON/memory store as a single flat property literally named "<tenantid>\IntuneManager", while the registry stored the same path as nested keys. The cause was "a\b".Split(@('/','\')) not splitting at all - PowerShell binds a string array to a different String.Split overload and hands back the whole path as one element. It is now [char[]]-cast and splits properly.

Existing settings files keep working: Get-SettingsTreeNode resolves a flat property first and writes back to it when it finds one, so no tenant-specific value is orphaned by the fix. New values nest.

Testing

Settings leak from the developer machine into tests, and tests leak into the developer machine. Tests/TestBootstrap.ps1 has the two helpers:

Describe 'x' {
    BeforeEach { $script:saved = Use-TestSettingsStore }
    AfterEach  { Restore-TestSettingsStore -State $script:saved }

    InModuleScope IntuneManagement { It 'y' { ... } }
}

Use-TestSettingsStore swaps in an empty memory store (so every key resolves to its registered default) and returns the previous state for Restore-TestSettingsStore.

The hooks must be declared at Describe level, wrapping InModuleScope. Pester 4 silently never runs a BeforeEach/AfterEach declared inside an InModuleScope block - nothing fails, the tests just run against the developer's real store and write to it. Static.Tests.ps1 has a gate for this with a shrink-only allowlist of the files that still get it wrong.

Tests/SettingsStore.Tests.ps1 covers the store modes, path resolution, value shape, provenance, portability and the public surface.

Auditing

Tools/Audit-Settings.ps1 cross-references every registration against every read and write. It is wired into Static.Tests.ps1 as a gate; see its help for the full taxonomy. Errors: orphan registrations, engine-consumed-but-UI-registered (breaks headless sessions), SubPath-mismatched access, duplicate registrations. Warnings: UI-only-but-engine-registered, and registered keys addressed by path instead of through the resolver.

.\Tools\Audit-Settings.ps1
.\Tools\Audit-Settings.ps1 -IncludeUndefined   # also keys read but never registered