# PowerShell script to detect idle folders on Windows

In a corporate environment, employees frequently access, create, and manage large volumes of data, resulting in numerous open folders in their devices. If the folders remain open, it can create a significant security vulnerability, potentially exposing sensitive company data to unauthorized access. Moreover, open but idle folders may result in corporate devices suffering from decreased performance, higher cost of maintenance, and disrupted data management. Identifying the idle time or the time for which the system folders were left open without usage can help administrators archive such folders, increasing security.

This document provides PowerShell scripts to recognize open folders for all local user accounts and further archive folders that have been idle beyond a specified time limit. The [Execute Custom Script](https://www.hexnode.com/mobile-device-management/help/executing-custom-scripts-for-windows/) remote action can help execute these PowerShell scripts on the target Windows devices.

 Warning 
The sample scripts provided below are adapted from third-party open-source sites.

 

 Supported Versions: 
The script is supported for execution on the following Windows versions:

- Windows 10 v1803+
- Windows 10 v1703 to Windows 10 v1709 (if .NET Framework v4.7.1+ is installed on the device)
- Windows 11 (Pro, Enterprise, Education)

 

Detect Open Folders for all User Accounts 
------------------------------------------

The following PowerShell script recurrently checks the last activity time of all the subfolders in the root paths (the `C:\` and `D:\` drives) of the device and calculates the idle time. The function used for this purpose is the `Get-FolderIdleTime` function, which performs the scan. The scanning progress is logged and can be viewed in the Show Output button of the Action History. It lists a table containing the folder path, last activity, time and idle days.

Detect folder idle time on Windows devices

function Get-FolderIdleTime { param( \[Parameter(Mandatory=$true)\] \[ValidateScript({Test-Path $\_ -PathType Container})\] \[string\]$TargetPath ) $LatestWriteTime = Get-ChildItem -Path $TargetPath -Recurse -File -Force -ErrorAction SilentlyContinue | Select-Object -ExpandProperty LastWriteTime | Sort-Object -Descending | Select-Object -First 1 if (-not $LatestWriteTime) { $LatestWriteTime = (Get-Item -Path $TargetPath -Force -ErrorAction SilentlyContinue).LastWriteTime } if (-not $LatestWriteTime) { $LatestWriteTime = Get-Date } $IdleTime = (Get-Date) - $LatestWriteTime \[PSCustomObject\]@{ FolderPath = $TargetPath LastActivity = $LatestWriteTime IdleDays = \[math\]::Round($IdleTime.TotalDays, 0) IdleTimeSpan = $IdleTime } } function Audit-IdleFolders { param( \[Parameter(Mandatory=$true)\] \[string\[\]\]$RootPaths ) $Report = @() Write-Host "Starting comprehensive audit of all subfolders..." -ForegroundColor Cyan foreach ($RootPath in $RootPaths) { Write-Host "`n-- Analyzing Root Path: $RootPath --" -ForegroundColor Yellow $FoldersToScan = Get-ChildItem -Path $RootPath -Directory -Force -ErrorAction SilentlyContinue foreach ($Folder in $FoldersToScan) { Write-Host " Checking Folder: $($Folder.Name)" -ForegroundColor Green $FolderStatus = Get-FolderIdleTime -TargetPath $Folder.FullName $Report += \[PSCustomObject\]@{ FolderPath = $FolderStatus.FolderPath LastActivity = $FolderStatus.LastActivity IdleDays = $FolderStatus.IdleDays } } } return $Report } $TargetAuditPaths = @( "C:\\Users", "D:\\SharedData" ) $ValidAuditPaths = @() foreach ($Path in $TargetAuditPaths) { if (Test-Path $Path -PathType Container) { $ValidAuditPaths += $Path } else { Write-Warning "Skipping invalid or inaccessible root path for audit: $Path." } } if ($ValidAuditPaths.Count -gt 0) { Audit-IdleFolders -RootPaths $ValidAuditPaths | Format-Table -AutoSize } else { Write-Error "No valid root paths were supplied for audit. Script aborted." }

   1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

  function Get-FolderIdleTime {

 param(

 \[Parameter(Mandatory=$true)\]

 \[ValidateScript({Test-Path $\_ -PathType Container})\]

 \[string\]$TargetPath

 )

 $LatestWriteTime = Get-ChildItem -Path $TargetPath -Recurse -File -Force -ErrorAction SilentlyContinue |

 Select-Object -ExpandProperty LastWriteTime |

 Sort-Object -Descending |

 Select-Object -First 1

 if (-not $LatestWriteTime) {

 $LatestWriteTime = (Get-Item -Path $TargetPath -Force -ErrorAction SilentlyContinue).LastWriteTime

 }

 if (-not $LatestWriteTime) {

 $LatestWriteTime = Get-Date

 }

 $IdleTime = (Get-Date) - $LatestWriteTime

 \[PSCustomObject\]@{

 FolderPath = $TargetPath

 LastActivity = $LatestWriteTime

 IdleDays = \[math\]::Round($IdleTime.TotalDays, 0)

 IdleTimeSpan = $IdleTime

 }

}

function Audit-IdleFolders {

 param(

 \[Parameter(Mandatory=$true)\]

 \[string\[\]\]$RootPaths

 )

 $Report = @()

 Write-Host "Starting comprehensive audit of all subfolders..." -ForegroundColor Cyan

 foreach ($RootPath in $RootPaths) {

 Write-Host "`n-- Analyzing Root Path: $RootPath --" -ForegroundColor Yellow

 $FoldersToScan = Get-ChildItem -Path $RootPath -Directory -Force -ErrorAction SilentlyContinue

 foreach ($Folder in $FoldersToScan) {

 Write-Host " Checking Folder: $($Folder.Name)" -ForegroundColor Green

 $FolderStatus = Get-FolderIdleTime -TargetPath $Folder.FullName

 $Report += \[PSCustomObject\]@{

 FolderPath = $FolderStatus.FolderPath

 LastActivity = $FolderStatus.LastActivity

 IdleDays = $FolderStatus.IdleDays

 }

 }

 }

 return $Report

}

$TargetAuditPaths = @(

 "C:\\Users",

 "D:\\SharedData"

)

$ValidAuditPaths = @()

foreach ($Path in $TargetAuditPaths) {

 if (Test-Path $Path -PathType Container) {

 $ValidAuditPaths += $Path

 } else {

 Write-Warning "Skipping invalid or inaccessible root path for audit: $Path."

 }

}

if ($ValidAuditPaths.Count -gt 0) {

 Audit-IdleFolders -RootPaths $ValidAuditPaths | Format-Table -AutoSize

} else {

 Write-Error "No valid root paths were supplied for audit. Script aborted."

}

   

 

  

The output of the script listing the detected open folders can be viewed in the **Show Output** button from **Manage > Your Windows device > Action History**.

[![Idle folders detected on Windows devices.](https://cdn.hexnode.com/mobile-device-management/help/wp-content/uploads/2025/11/Idle-folders-detected-on-Windows-devices.png)](https://cdn.hexnode.com/mobile-device-management/help/wp-content/uploads/2025/11/Idle-folders-detected-on-Windows-devices.png "Idle folders detected on Windows devices")

Archive Open Folders for all Users beyond a specified duration 
---------------------------------------------------------------

The following PowerShell script recursively checks the last activity time of all the subfolders in the root paths and calculates the idle time, like the previous script. The threshold value is set at 3 days, beyond which the folder is considered idle. The idle folders are then archived to a designated archive location `(C:\Data\Archive)`, preserving the internal folder structure (if subfolders are present) and the original user details associated with the folder (user who created the folder, user who last modified, etc.).

Archive Open folders on Windows devices

function Get-FolderIdleTime { param( \[Parameter(Mandatory=$true)\] \[ValidateScript({Test-Path $\_ -PathType Container})\] \[string\]$TargetPath ) $LatestWriteTime = Get-ChildItem -Path $TargetPath -Recurse -File -Force -ErrorAction SilentlyContinue | Select-Object -ExpandProperty LastWriteTime | Sort-Object -Descending | Select-Object -First 1 if (-not $LatestWriteTime) { $LatestWriteTime = (Get-Item -Path $TargetPath -Force -ErrorAction SilentlyContinue).LastWriteTime } if (-not $LatestWriteTime) { $LatestWriteTime = Get-Date } $IdleTime = (Get-Date) - $LatestWriteTime \[PSCustomObject\]@{ FolderPath = $TargetPath LastActivity = $LatestWriteTime IdleDays = \[math\]::Round($IdleTime.TotalDays, 0) IdleTimeSpan = $IdleTime } } function Close-IdleFolders { param( \[Parameter(Mandatory=$true)\] \[string\[\]\]$RootPaths, \[Parameter(Mandatory=$true)\] \[int\]$ArchiveThresholdDays, \[Parameter(Mandatory=$true)\] \[ValidateScript({Test-Path $\_ -PathType Container})\] \[string\]$ArchiveDestination ) # System-critical folders to exclude $ExcludedFolders = @( 'Public', 'Default', 'Default User', 'All Users', 'Administrator', 'defaultuser0', 'systemprofile', 'LocalService', 'NetworkService' ) $Report = @() $MaxDate = (Get-Date).AddDays(-$ArchiveThresholdDays) Write-Host "WARNING: This action targets ALL subfolders for ARCHIVING (moving)!" -ForegroundColor Red Write-Host "Folders not touched since $MaxDate will be moved to archive." -ForegroundColor Red Write-Host "Starting archive process with a threshold of $ArchiveThresholdDays days..." -ForegroundColor Cyan foreach ($RootPath in $RootPaths) { Write-Host "`n-- Scanning Root Path: $RootPath --" -ForegroundColor Yellow $Profiles = Get-ChildItem -Path $RootPath -Directory -Force -ErrorAction SilentlyContinue foreach ($Profile in $Profiles) { # Skip system-critical folders if ($ExcludedFolders -contains $Profile.Name) { Write-Host " SKIPPING System Folder: $($Profile.Name)" -ForegroundColor Magenta continue } Write-Host " Analyzing Root Folder: $($Profile.Name)" -ForegroundColor Green $SubFolders = Get-ChildItem -Path $Profile.FullName -Directory -Force -ErrorAction SilentlyContinue | Where-Object { $\_.FullName -ne $Profile.FullName } foreach ($SubFolder in $SubFolders) { $FullSubPath = $SubFolder.FullName Write-Host " Checking Subfolder: $($SubFolder.Name)" $FolderStatus = Get-FolderIdleTime -TargetPath $FullSubPath if ($FolderStatus.LastActivity -lt $MaxDate) { Write-Host " ACTION REQUIRED: $($FolderStatus.FolderPath) is idle for $($FolderStatus.IdleDays) days. ARCHIVING..." -ForegroundColor Yellow $ArchiveProfilePath = Join-Path -Path $ArchiveDestination -ChildPath $Profile.Name $NewPath = Join-Path -Path $ArchiveProfilePath -ChildPath $SubFolder.Name try { if (-not (Test-Path $ArchiveProfilePath)) { New-Item -Path $ArchiveProfilePath -ItemType Directory -Force | Out-Null } Move-Item -Path $FullSubPath -Destination $NewPath -Force -Verbose $Status = "ARCHIVED" $MoveTarget = $NewPath } catch { Write-Host " ERROR: Failed to move folder to archive: $($\_.Exception.Message)" -ForegroundColor Red $Status = "MOVE\_FAILED" $MoveTarget = "" } } else { Write-Host " Skipping: $($FolderStatus.FolderPath) is active (last activity: $($FolderStatus.LastActivity))." $Status = "ACTIVE" $MoveTarget = "" } $Report += \[PSCustomObject\]@{ Folder = $FolderStatus.FolderPath IdleDays = $FolderStatus.IdleDays Threshold = $ArchiveThresholdDays Action = $Status NewPath = $MoveTarget } } } } Write-Host "`nArchive process completed." -ForegroundColor Green return $Report } $TargetScanPaths = @( "C:\\Users" ) $ArchiveLocation = "C:\\Data\\Archive" $Threshold = 3 $ValidPaths = @() foreach ($Path in $TargetScanPaths) { if (Test-Path $Path -PathType Container) { $ValidPaths += $Path } else { Write-Warning "Skipping invalid or inaccessible root path for action: $Path." } } if (-not (Test-Path $ArchiveLocation)) { Write-Host "Creating archive directory: $ArchiveLocation" -ForegroundColor Cyan New-Item -Path $ArchiveLocation -ItemType Directory -Force } if ($ValidPaths.Count -gt 0) { Close-IdleFolders -RootPaths $ValidPaths -ArchiveThresholdDays $Threshold -ArchiveDestination $ArchiveLocation | Format-Table -AutoSize } else { Write-Error "No valid root paths were supplied for action. Script aborted." } 

   1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

  function Get-FolderIdleTime {

 param(

 \[Parameter(Mandatory=$true)\]

 \[ValidateScript({Test-Path $\_ -PathType Container})\]

 \[string\]$TargetPath

 )

 $LatestWriteTime = Get-ChildItem -Path $TargetPath -Recurse -File -Force -ErrorAction SilentlyContinue |

 Select-Object -ExpandProperty LastWriteTime |

 Sort-Object -Descending |

 Select-Object -First 1

 if (-not $LatestWriteTime) {

 $LatestWriteTime = (Get-Item -Path $TargetPath -Force -ErrorAction SilentlyContinue).LastWriteTime

 }

 if (-not $LatestWriteTime) {

 $LatestWriteTime = Get-Date

 }

 $IdleTime = (Get-Date) - $LatestWriteTime

 \[PSCustomObject\]@{

 FolderPath = $TargetPath 

 LastActivity = $LatestWriteTime 

 IdleDays = \[math\]::Round($IdleTime.TotalDays, 0)

 IdleTimeSpan = $IdleTime

 }

}

function Close-IdleFolders {

 param(

 \[Parameter(Mandatory=$true)\]

 \[string\[\]\]$RootPaths,

 \[Parameter(Mandatory=$true)\]

 \[int\]$ArchiveThresholdDays,

 \[Parameter(Mandatory=$true)\]

 \[ValidateScript({Test-Path $\_ -PathType Container})\]

 \[string\]$ArchiveDestination

 )

 \# System-critical folders to exclude 

 $ExcludedFolders = @(

 'Public',

 'Default',

 'Default User',

 'All Users',

 'Administrator',

 'defaultuser0',

 'systemprofile',

 'LocalService',

 'NetworkService'

 )

 $Report = @()

 $MaxDate = (Get-Date).AddDays(-$ArchiveThresholdDays)

 Write-Host "WARNING: This action targets ALL subfolders for ARCHIVING (moving)!" -ForegroundColor Red 

 Write-Host "Folders not touched since $MaxDate will be moved to archive." -ForegroundColor Red 

 Write-Host "Starting archive process with a threshold of $ArchiveThresholdDays days..." -ForegroundColor Cyan 

 foreach ($RootPath in $RootPaths) {

 Write-Host "`n-- Scanning Root Path: $RootPath --" -ForegroundColor Yellow

 $Profiles = Get-ChildItem -Path $RootPath -Directory -Force -ErrorAction SilentlyContinue 

 foreach ($Profile in $Profiles) {

 \# Skip system-critical folders 

 if ($ExcludedFolders -contains $Profile.Name) {

 Write-Host " SKIPPING System Folder: $($Profile.Name)" -ForegroundColor Magenta 

 continue

 }

 Write-Host " Analyzing Root Folder: $($Profile.Name)" -ForegroundColor Green

 $SubFolders = Get-ChildItem -Path $Profile.FullName -Directory -Force -ErrorAction SilentlyContinue |

 Where-Object { $\_.FullName -ne $Profile.FullName }

 foreach ($SubFolder in $SubFolders) {

 $FullSubPath = $SubFolder.FullName 

 Write-Host " Checking Subfolder: $($SubFolder.Name)"

 $FolderStatus = Get-FolderIdleTime -TargetPath $FullSubPath 

 if ($FolderStatus.LastActivity -lt $MaxDate) {

 Write-Host " ACTION REQUIRED: $($FolderStatus.FolderPath) is idle for $($FolderStatus.IdleDays) days. ARCHIVING..." -ForegroundColor Yellow

 $ArchiveProfilePath = Join-Path -Path $ArchiveDestination -ChildPath $Profile.Name

 $NewPath = Join-Path -Path $ArchiveProfilePath -ChildPath $SubFolder.Name 

 try {

 if (-not (Test-Path $ArchiveProfilePath)) {

 New-Item -Path $ArchiveProfilePath -ItemType Directory -Force | Out-Null

 }

 Move-Item -Path $FullSubPath -Destination $NewPath -Force -Verbose

 $Status = "ARCHIVED"

 $MoveTarget = $NewPath

 }

 catch {

 Write-Host " ERROR: Failed to move folder to archive: $($\_.Exception.Message)" -ForegroundColor Red

 $Status = "MOVE\_FAILED"

 $MoveTarget = ""

 }

 }

 else {

 Write-Host " Skipping: $($FolderStatus.FolderPath) is active (last activity: $($FolderStatus.LastActivity))."

 $Status = "ACTIVE"

 $MoveTarget = ""

 }

 $Report += \[PSCustomObject\]@{

 Folder = $FolderStatus.FolderPath 

 IdleDays = $FolderStatus.IdleDays 

 Threshold = $ArchiveThresholdDays 

 Action = $Status 

 NewPath = $MoveTarget

 }

 }

 }

 }

 Write-Host "`nArchive process completed." -ForegroundColor Green 

 return $Report

}

$TargetScanPaths = @(

 "C:\\Users"

)

$ArchiveLocation = "C:\\Data\\Archive"

$Threshold = 3

$ValidPaths = @()

foreach ($Path in $TargetScanPaths) {

 if (Test-Path $Path -PathType Container) {

 $ValidPaths += $Path

 } else {

 Write-Warning "Skipping invalid or inaccessible root path for action: $Path."

 }

}

if (-not (Test-Path $ArchiveLocation)) {

 Write-Host "Creating archive directory: $ArchiveLocation" -ForegroundColor Cyan 

 New-Item -Path $ArchiveLocation -ItemType Directory -Force

}

if ($ValidPaths.Count -gt 0) {

 Close-IdleFolders -RootPaths $ValidPaths -ArchiveThresholdDays $Threshold -ArchiveDestination $ArchiveLocation | Format-Table -AutoSize

} else {

 Write-Error "No valid root paths were supplied for action. Script aborted."

} 

   

 

  

A summary of the archived idle folders (open idly beyond 3 days) can be accessed from the **Show Output** button in the Hexnode UEM console. Navigate to **Manage > Your device > Action History > Execute Custom Script** action status.

[![Archiving idle folders on Windows devices.](https://cdn.hexnode.com/mobile-device-management/help/wp-content/uploads/2025/11/Archive-idle-folders-on-Windows.png)](https://cdn.hexnode.com/mobile-device-management/help/wp-content/uploads/2025/11/Archive-idle-folders-on-Windows.png "Archive idle folders on Windows")

[![List of idle folders detected and archived on Windows.](https://cdn.hexnode.com/mobile-device-management/help/wp-content/uploads/2025/11/List-of-idle-folders-archived-on-Windows.png)](https://cdn.hexnode.com/mobile-device-management/help/wp-content/uploads/2025/11/List-of-idle-folders-archived-on-Windows.png "List of idle folders archived on Windows")

 Notes:- It is recommended to manually validate the script execution on a system before executing the action in bulk.
- Hexnode will not be responsible for any damage/loss to the system on the behavior of the script.