Compress-Archive: the files missing from the zip you sent

Support asked for the logs. You zipped the folder, attached it, and the reply says most of it is not there. The archive really is incomplete, and Compress-Archive built it without an error, a warning or anything in the output to read.

The cause sits in one parameter. Compress-Archive -Path accepts wildcards, and its reference page documents three different results for the same folder depending on how you spell it. Two of the three are useful. The third drops every subfolder, and, measured below, one more file than the page mentions.

What follows builds a small folder, compresses it three ways, and reads each archive back without extracting it, so the difference is something you can see rather than something you have to take on trust. It also covers the thing -Update does not do, and where Expand-Archive puts the files at the far end.

Applies to: PowerShell 7 on Windows 10, Windows 11 and Windows Server. Measured on 7.4.6.


What you are going to build

One folder of your own, C:\bat\ziplab, holding a folder called Report with five files in two folders. From that you build three zip files, and you save one script, Show-ArchiveEntries.ps1, which prints what is inside an archive without extracting it.

Nothing outside C:\bat\ziplab is touched, and the last section deletes the whole folder in one command. That is the only script you are asked to keep.


Quick answer

If you only need the right spelling, it is the first of these two. Run them after the next section has created the folder. They write their own archive names, so they do not collide with the three measured further down, and neither line prints anything.

# The folder itself becomes the top level inside the zip, so whoever opens it
# gets one folder rather than five loose files in their downloads.
Compress-Archive -Path .\Report -DestinationPath .\Report.zip

# The folder's contents go in at the top level instead. Subfolders are kept.
Compress-Archive -Path .\Report\* -DestinationPath .\Report-contents.zip
Common mistake: Spelling the same folder .\Report\*.* because it looks like "everything in there". It is the narrowest of the three, it leaves out every subfolder, and it reports nothing while it does so.

Before you start

Three things, one command each.

  • PowerShell 7. Confirm the version with $PSVersionTable.PSVersion. Everything on this page was measured on 7.4.6, and the upgrade guide covers getting there.
  • A folder of your own to work in, so that nothing here goes near anything you care about. Every path after this one is relative to it.
  • Permission to run the one .ps1 this page saves. The reference for Set-ExecutionPolicy describes the Process scope like this: "The Process scope only affects the current PowerShell session."
# The three Path rules below are documented the same way for Windows PowerShell
# 5.1, so the spelling matters there too.
$PSVersionTable.PSVersion

# A folder of your own. Everything after this line uses relative paths.
New-Item -ItemType Directory -Path 'C:\bat\ziplab' -Force | Out-Null
Set-Location 'C:\bat\ziplab'

# Lets the saved script run, in this shell only.
Set-ExecutionPolicy -Scope Process -ExecutionPolicy RemoteSigned -Force

The first line prints your version. The other three print nothing.


Build the folder to compress

Five files in two folders is enough to tell the three spellings apart: three files at the top level of Report, and a Logs subfolder with two more. One of the five, CHANGELOG, has no extension, which turns out to matter.

# Five files in two folders. CHANGELOG has no extension on purpose.
New-Item -ItemType Directory -Path '.\Report\Logs' -Force | Out-Null
'service started'    | Set-Content -LiteralPath '.\Report\Logs\service.log'
'service restarted'  | Set-Content -LiteralPath '.\Report\Logs\service.1.log'
'ticket 4821'        | Set-Content -LiteralPath '.\Report\summary.txt'
'see summary'        | Set-Content -LiteralPath '.\Report\notes.txt'
'2026-10-07 rebuilt' | Set-Content -LiteralPath '.\Report\CHANGELOG'
Get-ChildItem -Path '.\Report' -Name | Sort-Object
'  in Logs:'
Get-ChildItem -Path '.\Report\Logs' -Name | Sort-Object

What to look for: four names at the top level of Report, one of which is the Logs folder, then the two files inside it.

CHANGELOG
Logs
notes.txt
summary.txt
  in Logs:
service.1.log
service.log

The script that reads an archive

Save this as Show-ArchiveEntries.ps1 in C:\bat\ziplab and run it with .\Show-ArchiveEntries.ps1 -ArchivePath .\root.zip. The parameter is mandatory because there is no sensible default for it. The script opens the archive read-only and extracts nothing, so it is safe to point at a zip you are about to send.

# Show-ArchiveEntries.ps1
# Prints what is inside a .zip file without extracting it.
# Separators are printed as a forward slash, so one archive always reads
# the same way here no matter which tool wrote it.
param(
    [Parameter(Mandatory = $true)]
    [string]$ArchivePath
)

$full = (Resolve-Path -LiteralPath $ArchivePath).Path
$zip  = [System.IO.Compression.ZipFile]::OpenRead($full)
try {
    $names = @($zip.Entries | ForEach-Object { $_.FullName -replace '\\', '/' } | Sort-Object)
    '{0,-10}{1}' -f 'archive', (Split-Path -Leaf $full)
    '{0,-10}{1}' -f 'entries', $names.Count
    foreach ($name in $names) { '  ' + $name }
}
finally {
    $zip.Dispose()
}

The one line worth explaining replaces a backslash with a forward slash in each printed name. That keeps every listing further down the page comparable with every other one.


Three spellings, three archives

Same folder, three spellings, one Compress-Archive call each, then the script reads all three back. The whole point of this block is the entries line in each listing.

# Same folder, three spellings, three archives.
Compress-Archive -Path .\Report     -DestinationPath .\root.zip
Compress-Archive -Path .\Report\*   -DestinationPath .\star.zip
Compress-Archive -Path .\Report\*.* -DestinationPath .\stardotstar.zip

# Read each one back without extracting it.
.\Show-ArchiveEntries.ps1 -ArchivePath .\root.zip
''
.\Show-ArchiveEntries.ps1 -ArchivePath .\star.zip
''
.\Show-ArchiveEntries.ps1 -ArchivePath .\stardotstar.zip

What to look for: two of the archives hold five entries and one holds two.

archive   root.zip
entries   5
  Report/CHANGELOG
  Report/Logs/service.1.log
  Report/Logs/service.log
  Report/notes.txt
  Report/summary.txt

archive   star.zip
entries   5
  CHANGELOG
  Logs/service.1.log
  Logs/service.log
  notes.txt
  summary.txt

archive   stardotstar.zip
entries   2
  notes.txt
  summary.txt
Result: root.zip carries all five files with Report/ in front of each name. star.zip carries the same five files with nothing in front. Those are the two spellings that keep your data.

The difference between those two is only where the files sit once the archive is opened, and the reference page says so directly. Its first bullet reads: "To create an archive that includes the root directory, and all its files and subdirectories, specify the root directory in the Path without wildcards."


What star-dot-star actually matches

The third bullet on the same page reads: "To create an archive that only zips the files in the root directory, use the star-dot-star (*.*) wildcard. Subdirectories of the root aren't included in the archive." The part about subdirectories is exactly what the measurement shows. The part before it is where the trouble is.

* matches zero or more characters, which about_Wildcards states as the first rule in its list. So *.* matches a name that contains a dot. CHANGELOG is a file in the root directory, it contains no dot, and it is not in stardotstar.zip. The bullet does not mention that.

Spelling of the same folderEntries in the archiveTop level inside the zipThe Logs subfolderThe file with no extension
-Path .\Report5Report/keptkept
-Path .\Report\*5the files themselveskeptkept
-Path .\Report\*.*2the files themselvesgonegone
Common mistake: Reading *.* the way cmd.exe reads it, as "every file". In PowerShell it is a pattern that needs a literal dot in the name. Three of the five files in this lab are missing from that archive: two because they live in a subfolder, and one because nobody ever gave it an extension.

Hidden files and folders never make it in

This one is not a wildcard question and it catches people who got the spelling right. The reference page carries it as a note: "The Compress-Archive cmdlet ignores hidden files and folders when creating or updating the archive file."

So a folder somebody marked hidden years ago, or a hidden file that an application wrote next to its logs, is simply absent from the archive. The reference states it plainly, which is why there is no measurement to add here, and it offers one way out: "To ensure hidden files and folders are compressed into the archive, use the .NET API instead."

Warning: The reference mentions no warning and no error for this, so reading what is in the archive is how you find out. Checking that the command succeeded tells you nothing about it.

Update adds and replaces, it never removes

A week later the ticket moves on. One file should no longer be in the bundle, one new file should, and rebuilding the archive with -Update looks like the obvious move.

# The file the far end should no longer receive.
Remove-Item -LiteralPath .\Report\notes.txt

# And one that did not exist when star.zip was built.
'closed 2026-10-07' | Set-Content -LiteralPath '.\Report\resolution.txt'

Compress-Archive -Path .\Report\* -DestinationPath .\star.zip -Update
.\Show-ArchiveEntries.ps1 -ArchivePath .\star.zip

What to look for: the entries line goes from five to six, and notes.txt is still listed although it no longer exists on disk.

archive   star.zip
entries   6
  CHANGELOG
  Logs/service.1.log
  Logs/service.log
  notes.txt
  resolution.txt
  summary.txt
Common mistake: Treating -Update as "make the archive match the folder". Its description covers replacing files that have the same name and adding files that are not there yet. Removal is not in it, and the measurement above is what that absence looks like.

When you want the archive to match the folder exactly, build it again with -Force instead, which the reference describes as the parameter that overwrites an existing archive file.


Expand-Archive and where the files land

The other half of the decision belongs to whoever opens the thing. Expand-Archive documents its default as a folder named after the zip: "By default, Expand-Archive creates a folder in the current location that's the same name as the ZIP file." Give it -DestinationPath and it uses that one instead. The same entry adds: "The target folder is created if it doesn't exist."

Expand-Archive -Path .\star.zip -DestinationPath .\OutStar
Expand-Archive -Path .\root.zip -DestinationPath .\OutRoot

'{0,-12}{1}' -f 'OutStar', ((Get-ChildItem -Path .\OutStar -Name | Sort-Object) -join ' ')
'{0,-12}{1}' -f 'OutRoot', ((Get-ChildItem -Path .\OutRoot -Name | Sort-Object) -join ' ')

What to look for: one destination holds five names, the other holds exactly one.

OutStar     CHANGELOG Logs notes.txt resolution.txt summary.txt
OutRoot     Report

OutStar holds the files themselves, including the notes.txt that the update never removed. OutRoot holds one entry, Report, with everything inside it. That single difference is the whole practical argument for zipping the folder rather than its contents: the person at the far end can extract it anywhere without scattering five files into whatever folder they were sitting in.

Note: Expand-Archive does not overwrite files that are already there. The reference gives -Force for that, described as the parameter to "overwrite existing files".

Clean up

The lab folder holds seven things from the sections above: the Report tree, the two folders the archives were expanded into, three zip files, and the script you saved. Two more if you ran the quick answer. One command removes all of it.

# Step out of the folder first.
Set-Location 'C:\'
Remove-Item -LiteralPath 'C:\bat\ziplab' -Recurse -Force

One thing is deliberately left behind: if C:\bat did not exist before you started, the setup command created it and this one does not remove it. The execution policy change needs no undoing, because it was scoped to the shell.


Tips and limitations

  • The three -Path rules are documented identically for Windows PowerShell 5.1, PowerShell 7.4 and PowerShell 7.5. This is not behaviour an upgrade changes.
  • -Path accepts wildcards. -LiteralPath takes the name exactly as typed, and it is the one to use when the path arrives in a variable.
  • Hidden files and folders are left out of the archive. The reference states that plainly and mentions no warning for it.
  • -Update adds and replaces. It does not remove. -Force rebuilds the archive instead.
  • Expand-Archive creates the destination folder if it is missing, and refuses to overwrite existing files unless you pass -Force.
  • Both reference pages state that the API behind these cmdlets limits the maximum file size to 2 GB. Neither says whether that means the archive or one file inside it, so treat 2 GB as the ceiling for both and reach for a different tool above it.
  • Read the archive before you send it. Hashing it at both ends tells you the transfer was clean, and it tells you nothing at all about whether the right files went in.

Official documentation


  • Hash generator: hash the archive before you send it and after it arrives, so a truncated transfer shows up as a different value.

Five cheat sheets, one PDF

Subnet masks, PowerShell, Linux commands, HTTP status codes and the ESXi command line - one page each, free to keep. Leave an address and it arrives in a minute.