# 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](SettingsCacheLogging.md). Every registered key, with type, default and description, is in [SettingsReference.md](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`: ```powershell 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). ```powershell Get-IMSettingsStore # Mode / Path / Persisted / ValueCount / RequestedMode Get-IMSettingsStore -IncludeValues # ... plus every SubPath/Key/Value row ``` ## Public API ```powershell 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 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: ```text 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: ```powershell 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: ```powershell 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 ` | switch to a `Json` store at that file, creating it if missing. | | `Use-IMSettingsStore -Registry` | switch back to `HKCU`. Windows only. | | `Export-IMSettingsStore ` | write the whole current store to a JSON file, whatever mode it is in - including a registry store. | | `Import-IMSettingsStore ` | 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 (`\IntuneManager`) used to be stored by the JSON/memory store as a **single flat property** literally named `"\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: ```powershell 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. ```powershell .\Tools\Audit-Settings.ps1 .\Tools\Audit-Settings.ps1 -IncludeUndefined # also keys read but never registered ```