diff --git a/system/Helpers/filesystem_helper.php b/system/Helpers/filesystem_helper.php
index 2556e86ade2a..0eb6851aa5a2 100644
--- a/system/Helpers/filesystem_helper.php
+++ b/system/Helpers/filesystem_helper.php
@@ -160,39 +160,48 @@ function write_file(string $path, string $data, string $mode = 'wb'): bool
 	 * @param string  $path    File path
 	 * @param boolean $del_dir Whether to delete any directories found in the path
 	 * @param boolean $htdocs  Whether to skip deleting .htaccess and index page files
-	 * @param integer $_level  Current directory depth level (default: 0; internal use only)
+	 * @param boolean $hidden  Whether to include hidden files (files beginning with a period)
 	 *
 	 * @return boolean
 	 */
-	function delete_files(string $path, bool $del_dir = false, bool $htdocs = false, int $_level = 0): bool
+	function delete_files(string $path, bool $del_dir = false, bool $htdocs = false, bool $hidden = false): bool
 	{
-		// Trim the trailing slash
-		$path = rtrim($path, '/\\');
+		$path = realpath($path) ?: $path;
+		$path = rtrim($path, DIRECTORY_SEPARATOR) . DIRECTORY_SEPARATOR;
 
 		try
 		{
-			$current_dir = opendir($path);
-
-			while (false !== ($filename = @readdir($current_dir)))
+			foreach (new RecursiveIteratorIterator(
+				new RecursiveDirectoryIterator($path, RecursiveDirectoryIterator::SKIP_DOTS),
+				RecursiveIteratorIterator::CHILD_FIRST
+			) as $object)
 			{
-				if ($filename !== '.' && $filename !== '..')
+				$filename = $object->getFilename();
+
+				if (! $hidden && $filename[0] === '.')
+				{
+					continue;
+				}
+				elseif (! $htdocs || ! preg_match('/^(\.htaccess|index\.(html|htm|php)|web\.config)$/i', $filename))
 				{
-					if (is_dir($path . DIRECTORY_SEPARATOR . $filename) && $filename[0] !== '.')
+					$isDir = $object->isDir();
+
+					if ($isDir && $del_dir)
 					{
-						delete_files($path . DIRECTORY_SEPARATOR . $filename, $del_dir, $htdocs, $_level + 1);
+						@rmdir($object->getPathname());
+						continue;
 					}
-					elseif ($htdocs !== true || ! preg_match('/^(\.htaccess|index\.(html|htm|php)|web\.config)$/i', $filename))
+
+					if (! $isDir)
 					{
-						@unlink($path . DIRECTORY_SEPARATOR . $filename);
+						@unlink($object->getPathname());
 					}
 				}
 			}
 
-			closedir($current_dir);
-
-			return ($del_dir === true && $_level > 0) ? @rmdir($path) : true;
+			return true;
 		}
-		catch (\Exception $fe)
+		catch (\Throwable $e)
 		{
 			return false;
 		}
diff --git a/tests/system/Helpers/FilesystemHelperTest.php b/tests/system/Helpers/FilesystemHelperTest.php
index a1cdba021c55..431fc123bb60 100644
--- a/tests/system/Helpers/FilesystemHelperTest.php
+++ b/tests/system/Helpers/FilesystemHelperTest.php
@@ -128,7 +128,7 @@ public function testDeleteFilesDefaultsToOneLevelDeep()
 		delete_files(vfsStream::url('root'));
 
 		$this->assertFalse($vfs->hasChild('simpleFile'));
-		$this->assertFalse($vfs->hasChild('.hidden'));
+		$this->assertTrue($vfs->hasChild('.hidden'));
 		$this->assertTrue($vfs->hasChild('foo'));
 		$this->assertTrue($vfs->hasChild('boo'));
 		$this->assertTrue($vfs->hasChild('AnEmptyFolder'));
@@ -143,7 +143,7 @@ public function testDeleteFilesHandlesRecursion()
 		delete_files(vfsStream::url('root'), true);
 
 		$this->assertFalse($vfs->hasChild('simpleFile'));
-		$this->assertFalse($vfs->hasChild('.hidden'));
+		$this->assertTrue($vfs->hasChild('.hidden'));
 		$this->assertFalse($vfs->hasChild('foo'));
 		$this->assertFalse($vfs->hasChild('boo'));
 		$this->assertFalse($vfs->hasChild('AnEmptyFolder'));
@@ -162,6 +162,29 @@ public function testDeleteFilesLeavesHTFiles()
 		delete_files(vfsStream::url('root'), true, true);
 
 		$this->assertFalse($vfs->hasChild('simpleFile'));
+		$this->assertTrue($vfs->hasChild('.hidden'));
+		$this->assertFalse($vfs->hasChild('foo'));
+		$this->assertFalse($vfs->hasChild('boo'));
+		$this->assertFalse($vfs->hasChild('AnEmptyFolder'));
+		$this->assertTrue($vfs->hasChild('.htaccess'));
+		$this->assertTrue($vfs->hasChild('index.html'));
+		$this->assertTrue($vfs->hasChild('index.php'));
+	}
+
+	public function testDeleteFilesIncludingHidden()
+	{
+		$structure = array_merge($this->structure, [
+			'.htaccess'  => 'Deny All',
+			'index.html' => 'foo',
+			'index.php'  => 'blah',
+		]);
+
+		$vfs = vfsStream::setup('root', null, $structure);
+
+		delete_files(vfsStream::url('root'), true, true, true);
+
+		$this->assertFalse($vfs->hasChild('simpleFile'));
+		$this->assertFalse($vfs->hasChild('.hidden'));
 		$this->assertFalse($vfs->hasChild('foo'));
 		$this->assertFalse($vfs->hasChild('boo'));
 		$this->assertFalse($vfs->hasChild('AnEmptyFolder'));