Repository navigation
User context, don't install to %OneDriveCommercial% if OneDrive for Business Known Folder Move (KFM) is enabled #627
Description
Activity
- changed the title
[-]User context, don't install to %OneDriveCommercial% if OneDrive for Business Known Folder Move (KFM) is enabled[/-][+]User context, don't install to ```%OneDriveCommercial%``` if OneDrive for Business Known Folder Move (KFM) is enabled[/+]on Apr 7, 2022 SydneyhSmith commented
on Apr 21, 2022 CollaboratorMore actionsThanks Olav Rønnestad Birkeland (@o-l-a-v) for opening this issue, we are investigating this-- to confirm have you hit this issue with our v3 previews?
o-l-a-v commented
on Apr 21, 2022 ContributorAuthorMore actionsDon't remember if I've tried with v3 yet.
Should be easy enough for you to reproduce? :)
o-l-a-v commented
on Apr 22, 2022 ContributorAuthorMore actionsTested with beta 3.0.12, it installs to
%OneDriveCommercial%\Documentsfrom both Windows PowerShell and PowerShell 7.2.2.\WindowsPowerShellwith Windows PowerShell,\PowerShellwith v7.2.2.- One more thing I noticed: Running the install command under first from Windows PowerShell, then with PowerShell v7.2.2, PowerShellGet says it's alrady installed. But it is really, when it get's installed to a different directory with PowerShell Core vs. Windows PowerShell? IMO, there needs to be some more thinking about both where to install, and detection logic.
# Import module downloaded from PowerShellGallery, extracted with 7-Zip Import-Module -Name ('{0}\powershellget.3.0.12-beta\PowerShellGet.psd1' -f [System.Environment]::GetFolderPath('Desktop')) # Check imported modules Get-Module # Install a module in user context PowerShellGet\Install-PSResource -Name 'Az.Cdn' -Scope 'CurrentUser' -Repository 'PSGallery' -Quiet -Confirm:$false -TrustRepository -Reinstall
o-l-a-v commented
on Apr 30, 2022 ContributorAuthorMore actionsSydneyhSmith commented
on May 12, 2022 CollaboratorMore actionsThanks Olav Rønnestad Birkeland (@o-l-a-v) we are planning to do a deeper dive into these (and other path) issues after our next release
Reacted by Olav Rønnestad Birkeland- changed the title
[-]User context, don't install to ```%OneDriveCommercial%``` if OneDrive for Business Known Folder Move (KFM) is enabled[/-][+]User context, don't install to `%OneDriveCommercial%` if OneDrive for Business Known Folder Move (KFM) is enabled[/+]on May 12, 2022 Honestly, whether or not the changes in PowerShell/15552 are made, having a parameter set which gives us the ability to specify the
-InstallPathinstead of a-Scopewhen callingInstall-PSResourcewould allow users who really want or need to do this to just change their$Env:PSModulePathand set a$PSDefaultParameterValuesto make it happen -- without needing to learn to useSave-PSResourceinstead.On company developer laptops, OneDrive routinely causes "Access to the cloud file is denied" errors when trying to upgrade modules there, and starts needlessly mirroring the files back to the cloud, slowing down install even more. Not to mention that every time I remove an old version of Az or Microsoft.Graph it causes that scary warning about how something has deleted thousands of files from my OneDrive...
Reacted by Olav Rønnestad Birkeland, BradCalvertLPNT and Martin GillI was more than happy to solve this in OneDrive "Choose Folders" option, but that doesn't work remotely the way I expected. I just wanted to exclude the Modules folder from syncing entirely but unchecking it in that screen does very different things than that.
Having modules roam between machines (via OneDrive) is desired functionality for me! Please don't turn it off!
I can see that some might want to turn it off, which they can at the moment by setting
$env:PSModulesPath.I can also see that some sort of alternative mechanism might be desired - eg. some sort of placeholder file, etc. although not all modules are available from the gallery, so quite a bit of design required there.
I would strongly recommend caution when breaking existing workflows by disabling/altering the currently enabled-by-default feature.
James May (@fowl2) thanks for reaching out and providing insight into your use case with this default behavior. This is being discussed and worked on from the PowerShell project side, on this issue linked here: PowerShell/PowerShell#15552
If you can share this comment there, that would be great thanks.
o-l-a-v commented
on Jan 12, 2023 ContributorAuthorMore actionsAfter some more tinkering I've found a solution that works for me.
1. Decide on desired directory
I settled on
%LOCALAPPDATA%\Microsoft\PowerShell\Modules.2. Create desired directory
The desired directory must be created prior to step 4.
Example code
$DesiredDirectory = [string] '{0}\Microsoft\PowerShell\Modules' -f $env:LOCALAPPDATA if (-not [System.IO.Directory]::Exists($DesiredDirectory)) { $null = [System.IO.Directory]::CreateDirectory($DesiredDirectory) }
3. Set user context environmental variable
PSModulePathto desired directoryImportant for PowerShell to automagically look for modules in desired directory.
Example code
# Assets $PSModulePathWanted = [string] '%LOCALAPPDATA%\Microsoft\PowerShell\Modules' $PSModulePathWantedResolved = [string] (cmd /c ('echo {0}' -f $PSModulePathWanted)) $RegistryPath = [string] 'Registry::HKEY_CURRENT_USER\Environment' # Create path if it does not exist if (-not [System.IO.Directory]::Exists($PSModulePathWantedResolved)) { $null = [System.IO.Directory]::CreateDirectory($PSModulePathWantedResolved) } # Get current value without resolving the path / expanding the environmental variable $PSModulePathCurrent = [string]( (Get-Item -Path $RegistryPath).GetValue( 'PSModulePath', '', 'DoNotExpandEnvironmentNames' ) ) # Make current PSModulePath to a string array for easier operations $PSModulePathNewAsArray = [string[]]( $PSModulePathCurrent.Split( [System.IO.Path]::PathSeparator ).Where{ -not [string]::IsNullOrEmpty($_) } ) # Remove "MyDocuments" if present, as it will resolve to OneDrive if Known Folder Move is enabled $PSModulePathNewAsArray = [string[]]( $PSModulePathNewAsArray.Where{ $_ -notlike ('{0}\*' -f [System.Environment]::GetFolderPath('MyDocuments')) } ) # Add $PSModulePathWanted if not already present if ($PSModulePathNewAsArray -notcontains $PSModulePathWanted) { $PSModulePathNewAsArray = [string[]]( [string[]]($PSModulePathWanted) + [string[]]($PSModulePathNewAsArray) | Where-Object -FilterScript { -not [string]::IsNullOrEmpty($_) } ) } # Convert $PSModulePathNewAsArray to string for easier comparison to existing value $PSModulePathNew = [string]($PSModulePathNewAsArray -join [System.IO.Path]::PathSeparator) # Set new value if it changed if ($PSModulePathNew -ne $PSModulePathCurrent) { $null = Set-ItemProperty -Path $RegistryPath -Name 'PSModulePath' -Value $PSModulePathNew -Force -Type ([Microsoft.Win32.RegistryValueKind]::ExpandString) }
4. Use
Save-Packagefor installing modulesUse
PackageManagementcmdletSave-Package, which let's you specify path.- Or PowerShellGet v2 cmdlet
Save-Module. - Or PowerShellGet v3 beta cmdlet
Save-Package.
Example code using PackageManagement
# Install a module ## Assets $DesiredDirectory = [string] '{0}\Microsoft\PowerShell\Modules' -f $env:LOCALAPPDATA $ModuleToInstall = [string] 'Az.Accounts' ## Create directory if it does not already exist if (-not [System.IO.Directory]::Exists($DesiredDirectory)) { $null = [System.IO.Directory]::CreateDirectory($DesiredDirectory) } ## Install module $null = PackageManagement\Save-Package -Type 'Module' -Source 'PSGallery' -Name $ModuleToInstall -Path $DesiredDirectory
Edit 1
I later found out that
PackageManagementandPowerShellGetdoes not actually use$env:PSModulePathwhen searching for installed modules, whileMicrosoft.PowerShell.Core\Get-ModuleandImport-Moduledoes. Opened an issue on this here:https://github.com/PowerShell/PowerShellGet/issues/889moved toGet-PSResourcedoes not search in user context environment variablePSModulePath#889
The problem with
Microsoft.PowerShell.Core\Get-Modulethough, for me at least, is that it does not return enough attributes on the module, like "Author".- "Author" is usefull when looking for child modules, say for
Az, and you want to filter out modules made by others thanMicrosoft Corporation.
Workaround for finding installed modules and versions to custom folder location.
### System context / AllUsers $ModulesDirectory = [string] '{0}\WindowsPowerShell\Modules' -f $env:ProgramW6432 ### User context / CurrentUser $ModulesDirectory = [string] '{0}\Microsoft\PowerShell\Modules' -f $env:LOCALAPPDATA ### Get installed modules $ModulesInstalledFromPSGallery = [PSCustomObject[]]( $( [array]( Get-ChildItem -Path $ModulesDirectory -Depth 0 -Directory ) ).ForEach{ Get-ChildItem -Path $_.'FullName' -Depth 0 -Directory | Where-Object -FilterScript { [System.IO.File]::Exists( ('{0}\PSGetModuleInfo.xml' -f $_.'FullName') ) } | Group-Object -Property 'Parent' }.ForEach{ [PSCustomObject]@{ 'Module' = [string] $_.'Name' 'Versions' = [System.Version[]] $_.'Group'.'Name' 'Author' = [string]( (Get-Content -Path ('{0}\PSGetModuleInfo.xml' -f $_.'Group'[0].'FullName') -Raw).Split( [System.Environment]::NewLine ).Where{ $_ -like '*<S N="Author">*' }.Trim().Split('>')[-2].Split('<')[0] ) 'Path' = [string] [System.IO.Directory]::GetParent($_.'Group'[0]) } } )
Edit 2
Got it faster by using .NET classes
Click to expand
### System context / AllUsers $ModulesDirectory = [string] '{0}\WindowsPowerShell\Modules' -f $env:ProgramW6432 ### User context / CurrentUser $ModulesDirectory = [string] '{0}\Microsoft\PowerShell\Modules' -f $env:LOCALAPPDATA $ModulesInstalledFromPSGallery = [PSCustomObject[]]( $( [string[]]([System.IO.Directory]::GetDirectories($ModulesDirectory)) ).ForEach{ $( [string[]]([System.IO.Directory]::GetDirectories($_)) ).Where{ [System.IO.File]::Exists(('{0}\PSGetModuleInfo.xml' -f $_)) } | Group-Object -Property @{'Expression'={[string]$_.Split([System.IO.Path]::DirectorySeparatorChar)[-2]}} }.ForEach{ [PSCustomObject]@{ 'Module' = [string] $_.'Name' 'Versions' = [System.Version[]]($_.'Group'.ForEach{$_.Split([System.IO.Path]::DirectorySeparatorChar)[-1]}) 'Author' = [string]( [System.IO.File]::ReadAllLines(('{0}\PSGetModuleInfo.xml' -f $_.'Group'[0])).Where{ $_ -like '*<S N="Author">*' }.Trim().Split('>')[-2].Split('<')[0] ) 'Path' = [string] [System.IO.Directory]::GetParent($_.'Group'[0]) } } )
Edit 3
Got it even faster by using
<string>.Contains()vs-like, and.Where({<filter>},'First').Click to expand
$ModulesInstalledFromPSGallery = [PSCustomObject[]]( $( [string[]]([System.IO.Directory]::GetDirectories($ModulesDirectory)) ).ForEach{ $( [string[]]([System.IO.Directory]::GetDirectories($_)) ).Where{ [System.IO.File]::Exists(('{0}\PSGetModuleInfo.xml' -f $_)) } | Group-Object -Property @{'Expression'={[string]$_.Split([System.IO.Path]::DirectorySeparatorChar)[-2]}} }.ForEach{ [PSCustomObject]@{ 'Module' = [string] $_.'Name' 'Versions' = [System.Version[]]($_.'Group'.ForEach{$_.Split([System.IO.Path]::DirectorySeparatorChar)[-1]}) 'Author' = [string]( [System.IO.File]::ReadAllLines(('{0}\PSGetModuleInfo.xml' -f $_.'Group'[0])).Where( {$_.Contains('<S N="Author">')}, 'First' ).Trim().Split('>')[-2].Split('<')[0] ) 'Path' = [string] [System.IO.Directory]::GetParent($_.'Group'[0]) } } )
- Or PowerShellGet v2 cmdlet
I have not researched the behavior of PowerShellGet v3, but in v2, when the
Update-Modulecommand calls theInstall-Modulecommand internally, it uses the value of theInstalledLocationfrom thePSGetModuleInfo.xmlfile located within the directory where the module is installed to determine the value of theScopeparameter. This file contains the full path of the module directory. However, if the document directory is synchronized with OneDrive and the same Microsoft account is used on multiple machines, or if the PC is re-setup and the username changes, it is possible that the value ofInstalledLocationand the actual path where the module exists may differ.Reacted by Olav Rønnestad BirkelandOlav Rønnestad Birkeland (@o-l-a-v) Can you provide a new link to the issue you mentioned in your first edit? It leads to a 404.
Is there any progress on this?
o-l-a-v commented
on May 19, 2025 ContributorAuthorMore actionsDaniel Niccoli (@danielniccoli): I think it must be this one: #889.
No progress that I'm aware of. I think I read something about the PowerShell working group having this up to discussion again recently. Don't remember where I read that. Maybe in the PowerShell repo. Edit: Maybe the PowerShell Community Call for May 2025 discusses this in agenda point "MyDocuments issue Justin Chung": PowerShell/PowerShell#25058. Recording is not out yet: https://www.youtube.com/@powershellanddscteamchanne5739/videos.
I also opened a PR with a suggestion for an optional override. The basic functionality works, but the repo owners haven't interacted with me on it yet.
Wall of links
Issues:
- 2018-10-18 "Move PowerShell folder on Windows from ${env:USERPROFILE}\OneDrive\Documents\PowerShell to ${env:USERPROFILE}.powershell": Move PowerShell folder on Windows from ${env:USERPROFILE}\OneDrive\Documents\PowerShell to ${env:USERPROFILE}\.powershell PowerShell#8069
- 2021-06-09 "Let's move PSModulePath out of the documents folder": Let's move PSModulePath out of the documents folder PowerShell#15552
- 2022-04-07 "User context, don't install to %OneDriveCommercial% if OneDrive for Business Known Folder Move (KFM) is enabled": User context, don't install to
%OneDriveCommercial%if OneDrive for Business Known Folder Move (KFM) is enabled #627 - 2023-01-12 "Get-PSResource does not search in user context environment variable PSModulePath":
Get-PSResourcedoes not search in user context environment variablePSModulePath#889 - 2023-11-16 "Allow to specify custom module path in Install-PSResource": Allow to specify custom module path in Install-PSResource #1494
- 2024-06-08 "Script installation should require less fiddling with PATH": Script installation should require less fiddling with PATH #1661
- 2025-02-26 "Move $PROFILE.CurrentUser* and friends out of documents": Move
$PROFILE.CurrentUser*and friends out of documents PowerShell#25097
PRs:
- 2024-07-13 "Idea for ability to optionally override default modules and scripts install path - Duscussion and contribution is welcome!": Idea for ability to optionally override default modules and scripts install path - Duscussion and contribution is welcome! #1673
Workarounds:
- Use
Microsoft.PowerShell.PSResourceGet\Save-PSResource -Path+ add new path to$env:PSModulePathin user context. - Use Justin Grote "ModuleFast": https://github.com/JustinGrote/ModuleFast
o-l-a-v commented
on May 20, 2025 ContributorAuthorMore actionsDaniel Niccoli (@danielniccoli)
Current status is a PR for a RFC, and mentioned in the yearly PowerShell blog post about future investments.
- 2025-04-03 PR for RFC0066: Move PS content out of OneDrive PowerShell-RFC#388
- 2025-04-14 "PowerShell, OpenSSH, and DSC team investments for 2025": https://devblogs.microsoft.com/powershell/powershell-openssh-and-dsc-team-investments-for-2025/#moving-powershell-content-folder-out-of-mydocuments
Reacted by Jan Hajek
Summary of the new feature / enhancement
Behavior today
Default install location for PowerShell scripts and modules when specifying user context, is:
%USERPROFILE%\Documents\WindowsPowerShell\Modules%USERPROFILE%\Documents\PowerShell\ModulesBut if you have OneDrive for Business set up with Known Folder Move (KFM), default install location for user context is:
%OneDriveCommercial%\Documents\WindowsPowerShell\Modules%OneDriveCommercial%\Documents\PowerShell\ModulesWhy is it a problem
This is not ideal, as you'll end up with hundreds or thousands of small files that will be synced up and down to OneDrive, which might cause OneDrive sync issues, and other performance hits.
I currently install all modules to AllUsers scope for this reason. Currently 2.7 GB, 12 617 files, 2 315 folders.
Screenshot
If I did not care about this myself, I'd be using more than 1 / 10 of the capacity / max number of files recommendation for the OneDrive client, just for PowerShell modules.
Proposed technical implementation details
In my opinion, there is no reason to install PowerShell modules from PowerShell Gallery to OneDrive by default when KFM is active. A publicly available PowerShell module is nothing unique that needs to be backed up/ synced.
Option 1 - Cmdlet to set PSResourceLocation for Process/User/Machine
Add cmdlet to set PSResourceLocation for scope Process/User/Machine. For instance:
It could also:
[System.Environment]::GetEnvironmentVariable('PSModulePath','<scope>').PackageManagement,PowerShellGetandMicrosoft.PowerShell.PSResourceGetfrom old path given scope to new path.Option 2 - Use first path in
$env:PSModulePathif setIf I've set
[System.Environment]::GetEnvironmentVariable('PSModulePath','User').Split(';')[0]to be somewhere else than the default location for<scope>, use it.Option 3 - Don't follow KFM redirect
Users must opt in to install PowerShell modules to OneDrive, instead of current default behavior.
%USERPROFILE%\Documents\WindowsPowerShell\Modules%USERPROFILE%\Documents\PowerShell\ModulesOption 4 - Change default location for user scope to
%LOCALAPPDATA%Change default location for user context to:
%LOCALAPPDATA%\WindowsPowerShell\Modules%LOCALAPPDATA%\PowerShell\Modules