In a fresh PowerShell session you run a cmdlet from a module you never imported and it just works. What is module auto-loading, and how does PSModulePath drive it?
answer
- a variable of directories, not a registry
- one subfolder per module name
- exports indexed without loading the code
- per-user scope, per-identity path
- service account searches somewhere else
basics
~20 sPowerShell scans the directories listed in the PSModulePath environment variable, indexes the commands each module exports, and imports the owning module automatically the first time you invoke one of those commands. Explicit Import-Module is only needed to force a specific module or version.
solid answer
~50 sSince PowerShell 3.0, you rarely call `Import-Module` by hand. PowerShell walks every directory in `$env:PSModulePath`, treats each subfolder as a module name, reads the manifest's export lists, and builds a map of command name to module. When you type a command it does not already have loaded, it finds the owning module in that map and imports it for you. That makes `PSModulePath` the whole story for discovery. It normally contains three tiers: a per-user folder under your profile, a machine-wide folder under Program Files, and the folder shipping the modules that came with PowerShell itself. `Get-Module -ListAvailable` shows exactly what discovery found. The practical consequence is the one interviewers probe: `Install-Module -Scope CurrentUser` puts the module in *your* profile, so a scheduled task or service running as a different account searches a different `PSModulePath` and reports "the term is not recognized". Install with `-Scope AllUsers`, or add the path deliberately, for anything another identity has to run.
code
powershell · 7 lines$env:PSModulePath -split [IO.Path]::PathSeparator
Get-Module -ListAvailable |
Select-Object Name, Version, ModuleBase |
Sort-Object Name
$PSModuleAutoloadingPreference = 'None' # nothing loads without Import-Modulego deeper
Know that PowerShell finds modules through the PSModulePath directories and loads them on first use, and that Get-Module -ListAvailable shows what is discoverable.
Explain the folder-per-module layout, the three default path tiers, and how command-to-module indexing is built from the manifest's export lists without running the module's code.
Demonstrate the operational judgment: choose the install scope for the identity that will actually run the code, pin imports in automation, and diagnose 'not recognized' by comparing PSModulePath between accounts.
Own how modules reach every execution context in the estate — build agents, scheduled tasks, containers — and decide between machine-wide installs, vendoring next to the code, and a baked image, knowing each one's drift and audit story.
## The behaviour Before PowerShell 3.0 you had to `Import-Module` anything you wanted to use. Since 3.0, invoking a command from a module that is merely *available* imports that module implicitly. The same applies to `Get-Command`, `Get-Help` and tab completion against a not-yet-loaded module. This is module auto-loading, and it is the reason a brand-new session appears to know thousands of commands. ## What discovery actually walks Discovery is driven entirely by the `PSModulePath` environment variable — a list of directories separated by `;` on Windows and `:` on Linux and macOS. ```powershell $env:PSModulePath -split [IO.Path]::PathSeparator Get-Module -ListAvailable | Select-Object Name, Version, ModuleBase ``` Inside each of those directories, every **subfolder** is treated as a candidate module named after the folder. PowerShell looks inside for `<Name>.psd1`, `<Name>.psm1` or `<Name>.dll` — or for versioned subfolders (`<Name>\1.4.0\<Name>.psd1`) when several versions are installed side by side. This is why a module folder whose name does not match the manifest inside it quietly fails to be found. On PowerShell 7 for Windows the default tiers are roughly: - `$HOME\Documents\PowerShell\Modules` — the current user - `$env:ProgramFiles\PowerShell\Modules` — all users - `$PSHOME\Modules` — the modules that ship with the engine Windows PowerShell 5.1 uses the `WindowsPowerShell` variants of the first two, which is why the two engines do not automatically see each other's installs. On Linux and macOS the equivalents live under `~/.local/share/powershell/Modules` and `/usr/local/share/powershell/Modules`. Order matters: earlier entries win when the same module name appears in more than one directory, so a per-user copy shadows a machine-wide one. ## The cost of discovery, and FunctionsToExport To map command names to modules, PowerShell must know what each module exports without loading it. It gets that from the manifest's `FunctionsToExport`, `CmdletsToExport` and `AliasesToExport` lists, which is cheap because a `.psd1` is parsed as restricted data. If a manifest declares `FunctionsToExport = '*'`, PowerShell cannot answer the question from the manifest alone and has to do far more work to discover the module's commands. On a machine with many such modules this shows up as sluggish tab completion and slow first-command latency. Enumerating exports explicitly is a real performance decision, not just tidiness. ## The failure everyone actually hits Auto-loading is per-identity because `PSModulePath` is per-identity: ```powershell Install-Module -Name Az.Accounts -Scope CurrentUser # lands in YOUR profile Install-Module -Name Az.Accounts -Scope AllUsers # lands under Program Files ``` Install as yourself with `-Scope CurrentUser`, then hand the script to a scheduled task running as a service account, a CI agent running as `NETWORK SERVICE`, or a colleague — and it fails with `The term 'Connect-AzAccount' is not recognized as the name of a cmdlet`. Nothing is broken: that identity's `PSModulePath` simply does not include your profile folder. The fix is to install for all users, vendor the module next to the script and import it by path, or extend `PSModulePath` for that context. A second variant: a process that inherited its environment before you changed `PSModulePath` will not see the new entry. Environment changes reach existing sessions only if you set the variable in-process. ## Controlling auto-loading `$PSModuleAutoloadingPreference` governs the behaviour. `'All'` is the default; `'ModuleQualified'` auto-loads only when you call the command fully qualified as `ModuleName\Command-Name`; `'None'` disables it entirely, so nothing loads without an explicit `Import-Module`. Turning it off is occasionally worth it in a tightly controlled automation host where you want no surprises about which module supplied a command. Explicit `Import-Module` still has jobs to do even with auto-loading on: pinning a version with `-RequiredVersion`, importing from a path outside `PSModulePath`, forcing a reload with `-Force` while developing, or renaming commands on import with `-Prefix` to avoid a name collision. ## Adding a directory ```powershell $env:PSModulePath = $env:PSModulePath + [IO.Path]::PathSeparator + 'C:\Automation\Modules' ``` That lasts for the session. To make it durable, set it in the profile for interactive users, or as a machine-level environment variable for service accounts — the profile is not read by every host, so relying on it for automation is a trap. ## What an interviewer is listening for That you can name `PSModulePath`, describe the folder-per-module layout, connect the per-user install scope to the "works for me, not for the service account" failure, and know that explicit `Import-Module` remains the tool for version pinning and out-of-path imports.
- Auto-loading is on, so why would you still write Import-Module explicitly in a production script?To pin a version with `-RequiredVersion`, to import a module that lives outside `PSModulePath` by full path, to fail fast at the top of the script rather than midway through, and to make the dependency visible to whoever reads the file. Auto-loading is a convenience for interactive use; explicit imports are documentation plus determinism.
- Why does a manifest with FunctionsToExport = '*' slow down a session?Discovery normally answers "which module owns this command?" by reading the manifest's export lists as data. A wildcard forces PowerShell to do much more work to enumerate the module's commands, so tab completion and first-command latency degrade — noticeably once several installed modules do it.
- You add a directory to PSModulePath but an already-running PowerShell session still cannot find the module. Why?Environment variables are copied into a process at start. Editing the user or machine variable affects processes launched afterwards, not the running session. Set `$env:PSModulePath` in-process to fix the current session, and set the persistent variable for future ones.
saying these in an interview costs you the question
- Thinks modules must always be imported explicitly
- Believes PSModulePath and PATH are the same list
- Says installing for the current user makes it available to every account
- Assumes any folder containing a .psm1 anywhere on disk is discoverable
- Thinks auto-loading executes the module's code just to list commands