Linux Zip and Unzip Commands: Stop Getting an Empty Archive

Featured banner showing a folder of files moving into an archive where one entry is filled and the next is an empty dashed slot

The zip and unzip commands are separate programs, and the one that packs your files returns exit code 0 whether or not it stored anything.

That is how you can run zip plain.zip project, read the single line adding: project/ (stored 0%), and finish with an archive that holds no files. I ran the whole set on Ubuntu 24.04.4 against zip 3.0 and unzip 6.00, and the output shown is what those runs returned.

Every example below is followed by the listing that shows what the archive contains, so you can tell a packed archive from an empty one before you send it.

Zip and unzip are two packages, and the archive only holds what you name

Both tools come from Info-ZIP and both install as separate binaries. A machine can carry zip without unzip, and the other way round.

An archive is a container with a table of entries, and each entry stores a path, a length, a timestamp, and either compressed bytes or the original bytes.

The method is chosen per entry. Deflate is the default and shortens text, while the stored method copies bytes unchanged, which is why files that are already compressed show up as stored.

Nothing enters that table unless a name you pass on the command line reaches it, and that one rule explains most of what surprises people about zip.

sudo apt install zip unzip

Check which build your distribution resolved to, because the options below are described against the packages that ship with the current Ubuntu LTS. I ran apt-cache policy zip unzip and this system resolved to the two versions below.

apt-cache policy zip unzip
apt-cache policy zip unzip showing zip 3.0-13ubuntu0.2 and unzip 6.0-28ubuntu4.1 installed on Ubuntu 24.04
The installed versions are what the rest of this article assumes.

What you need before the first archive

The install is the only step that needs root. Everything after it runs as an ordinary user inside a directory you already own.

  • zip 3.0 and unzip 6.0 or newer, which the current Ubuntu, Debian, and Fedora releases all ship
  • write permission on the directory where the archive is created
  • write permission on the extraction directory, which is a different place once you pass -d
  • a folder with a few files in it

Permission is the requirement that fails in practice, because a directory you can read is not always a directory you can write, and the chmod command is what changes those bits when the target belongs to you.

The fixture below builds a small project tree with a file in each folder. It is small enough to check by eye and nested enough to show whether recursion happened.

mkdir -p project/notes project/assets
printf 'print("hello from app")\n' > project/app.py
printf 'first note\n' > project/notes/todo.txt
printf 'a,b,c\n1,2,3\n' > project/assets/data.csv

Directories and files are stored as separate entries, so a listing of your practice set is the baseline the archive should match.

Listing Files For Zip
A listing of the practice set, with the directories marked by the leading d.

Create an archive that actually contains your files

The shape of the command never changes. The archive name comes first, then the names you want inside it, and the second group decides what the table holds.

Zip one file

Pass one file and you get one entry with its own length and its own method.

zip app.zip project/app.py
  adding: project/app.py (stored 0%)

The adding line names the entry as it lands, and the percentage is the space that compression saved on that particular file.

Compressing Files Using Zip
Several files on one line each get their own entry, and text that compresses well reports a deflated percentage.

Zip a folder and everything under it

A folder needs the recursion flag, because a directory name on its own is one name and therefore one entry.

zip -r project.zip project
  adding: project/ (stored 0%)
  adding: project/app.py (stored 0%)
  adding: project/assets/ (stored 0%)
  adding: project/assets/data.csv (stored 0%)
  adding: project/notes/ (stored 0%)
  adding: project/notes/todo.txt (stored 0%)

Read the entries back with unzip -l and the length column is the proof that the walk happened.

unzip -l project.zip
Archive:  project.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
        0  2026-09-11 10:47   project/
       24  2026-09-11 10:47   project/app.py
        0  2026-09-11 10:47   project/assets/
       12  2026-09-11 10:47   project/assets/data.csv
        0  2026-09-11 10:47   project/notes/
       11  2026-09-11 10:47   project/notes/todo.txt
---------                     -------
       47                     6 files
unzip -l project.zip listing six entries including app.py, assets/data.csv and notes/todo.txt
The same folder with -r, and now the files are inside the archive.

What a missing -r leaves behind

Drop the flag and the command still exits cleanly. What it stores is the folder name and nothing beneath it.

zip plain.zip project
  adding: project/ (stored 0%)
unzip -l plain.zip
Archive:  plain.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
        0  2026-09-11 10:47   project/
---------                     -------
        0                     1 file
unzip -l plain.zip listing one zero-length directory entry named project/ and a total of one file
One entry, zero bytes, and a clean exit status.

I ran that pair on Ubuntu 24.04.4 and the archive came back with exactly one entry of zero length at exit code 0, which is why the failure survives a quick look at the terminal.

The folder name is stored as an entry in its own right, so the listing looks plausible until you read the length column.

Compressing A Folder
Naming a folder on its own stores the folder entry and stops there.
Compressing Multiple Folders
Several folder names behave the same way, one bare entry each.

Pack the contents without the parent folder

Keeping the folder name is usually what you want, because the archive then unpacks into a tree instead of scattering files into the directory above it.

When the contents should sit at the root of the archive, change into the directory first and pass a dot.

cd project && zip -r ../contents.zip .
Archive:  ../contents.zip
  Length      Date    Time    Name
---------  ---------- -----   ----
       24  2026-09-11 10:47   app.py
        0  2026-09-11 10:47   assets/
       12  2026-09-11 10:47   assets/data.csv
        0  2026-09-11 10:47   notes/
       11  2026-09-11 10:47   notes/todo.txt
---------                     -------
       47                     5 files

The dot also reaches hidden files. A plain asterisk does not, so a folder that keeps its settings in a dotfile loses them from the archive.

Quiet the adding lines

The -q option removes the per-file lines, which is what you want when an archive holds hundreds of entries and the exit status is the only thing you read.

zip -q -r project.zip project
Compressing Files Using Zip With Quiet Option
Quiet mode prints nothing between the command and the next prompt, so check the listing instead.

Look inside an archive and extract it

You will unpack more archives than you pack, and most of them arrive from somewhere else. Inspection comes first because it tells you what extraction is about to write.

Inspect an archive without extracting it

Verbose mode adds the compression method, the stored size, and the CRC-32 checksum for every entry.

unzip -v project.zip
 Length   Method    Size  Cmpr    Date    Time   CRC-32   Name
--------  ------  ------- ---- ---------- ----- --------  ----
       0  Stored        0   0% 2026-09-11 10:47 00000000  project/
      24  Stored       24   0% 2026-09-11 10:47 8058b3da  project/app.py
       0  Stored        0   0% 2026-09-11 10:47 00000000  project/assets/
      12  Stored       12   0% 2026-09-11 10:47 bbbbb379  project/assets/data.csv
       0  Stored        0   0% 2026-09-11 10:47 00000000  project/notes/
      11  Stored       11   0% 2026-09-11 10:47 f3f5d60a  project/notes/todo.txt
--------          -------  ---                            -------
      47               47   0%                            6 files

When only the names matter, for a script or a quick check, unzip -Z1 prints one stored path per line with no table around it. It is the form to use inside a loop.

unzip -Z1 project.zip
project/
project/app.py
project/assets/
project/assets/data.csv
project/notes/
project/notes/todo.txt

Extract into the current directory or one you name

With no options, unzip writes every entry into the current directory and recreates the stored subdirectories underneath it.

unzip project.zip
Archive:  project.zip
  extracting: project/app.py
  extracting: project/assets/data.csv
  extracting: project/notes/todo.txt

The -d flag sends the whole tree somewhere else, which is the flag to reach for when the archive would otherwise drop its contents into your home directory.

unzip project.zip -d out
Archive:  project.zip
 extracting: out/project/
 extracting: out/project/app.py
 extracting: out/project/assets/
 extracting: out/project/assets/data.csv
 extracting: out/project/notes/
 extracting: out/project/notes/todo.txt
unzip project.zip -d out extracting the archive into the out directory with the project path preserved
The -d flag sends the whole tree into a directory you name.

The df command shows how much room that target filesystem has. That matters because unzip keeps writing until it runs out of space and then stops partway.

Archives also arrive as tarballs, bzip2 files, and older compress files, and extract all compressed files with a single command covers a folder full of mixed formats.

Decide what happens to files that already exist

The default is to ask before overwriting, one file at a time. That question is the reason an unattended script hangs.

FlagBehaviour on an existing file
(none)asks before each file, and waits for an answer
-ooverwrites without asking
-nskips files that already exist and creates nothing new
-freplaces files that are newer than the copy on disk, adds nothing
-udoes that and creates the files missing from disk

Answering a at the replace prompt applies that answer to every remaining file. The run finishes without further questions.

Unzip Without Options
The replace prompt names one file at a time until you answer with a or A.

Quiet mode does not silence that prompt. It removes the file list, and the question still waits for input.

Unzip Quietly
The file names are gone, the overwrite question is not.

With no terminal attached, the prompt reads end of file, unzip treats that as no, and the command exits with status 1. I tried it with the input closed and the archive was left exactly as it was.

When the target directory refuses the files

A directory you can read is not always a directory you can write, and unzip reports each path it cannot create instead of stopping at the first one.

unzip project.zip -d ro
Archive:  project.zip
checkdir error:  cannot create ro/project
                 Permission denied
                 unable to process project/.
checkdir error:  cannot create ro/project
                 Permission denied
                 unable to process project/app.py.

I tested the same command against a read-only target, and every entry failed the same way. The status came back as 2 while the archive itself is fine, and the exit code is the signal worth reading.

Once the files land correctly and you have several archives to handle, the same flags work across a list of them. The unzip multiple files walkthrough covers that loop.

Exclude entries, delete them, or update the archive in place

An archive is not fixed once it is written. The options below change what is inside an existing file without rebuilding it from the original folder.

Leave names out with -x

The exclude list matches the names on the command line, and it takes more than one entry, so a single flag covers several paths.

zip -r filtered.zip project -x 'project/notes/*'
  adding: project/ (stored 0%)
  adding: project/app.py (stored 0%)
  adding: project/assets/ (stored 0%)
  adding: project/assets/data.csv (stored 0%)

unzip takes the same flag, and there it matches the entry names stored in the archive rather than files on disk.

unzip project.zip -x 'project/assets/*' -d ex
Archive:  project.zip
   creating: ex/project/
 extracting: ex/project/app.py
   creating: ex/project/notes/
 extracting: ex/project/notes/todo.txt
Zip File Exclusion
One name on the exclude list is enough to keep that entry out of the archive.
Unzip File Exclusion
The same flag on the extraction side leaves the matching entries unwritten.

Delete an entry with zip -d

Deleting removes the entries whose names match and rewrites the archive around what is left, so the stored paths are what the list sees.

zip -d trim.zip 'project/assets/*'
deleting: project/assets/
deleting: project/assets/data.csv

That letter does double duty across the pair and the two meanings have nothing in common. In zip it deletes entries, and in unzip it names the directory to extract into.

Freshen or update without rebuilding

Two modes cover the case where the archive is behind the folder. Freshen replaces entries that changed, and update also adds the files the archive is missing.

zip -f project.zip project/notes/todo.txt
freshening: project/notes/todo.txt (stored 0%)
zip -u project.zip project/extra.txt
  adding: project/extra.txt (stored 0%)

unzip uses both modes on the extraction side, where the comparison runs against the files already on disk. Either way the archive keeps its existing entries, so neither mode wastes the compression work already done.

Zip File Freshening F Option
Freshening reports only the entries whose contents changed.
Zip Update File Option
Updating adds the file that was missing from the archive and leaves the rest alone.
Unzip Freshen And Update
On the extraction side the same two flags compare entries against the files on disk.

Password-protect an archive and verify it

Encryption is built into both tools, and so is the check that catches a damaged archive before you spend time extracting it.

Create an encrypted archive

The -e flag asks for a password twice and refuses to run when standard error is not a terminal, which is what keeps it out of a scripted job.

zip -e secret.zip project/app.py
Enter password:
Verify password:
  adding: project/app.py (stored 0%)

unzip asks for the same password when it opens the archive, on the first entry it needs. I ran both prompts from a terminal and the password never appears on screen while you type it.

unzip -o secret.zip project/app.py
Archive:  secret.zip
[secret.zip] project/app.py password:
 extracting: project/app.py

The method behind that prompt is the original zip encryption, and the unzip manual calls it relatively weak and points at PGP when the contents matter. Three limits follow from how it works.

  • It protects the file contents and leaves the stored paths, lengths, and timestamps readable
  • Anyone holding the archive can run a listing on it without the password
  • It holds off casual inspection rather than a determined attack on a short password

When the data needs stronger protection than that, 7-Zip writes AES-encrypted archives and the 7zip files on Ubuntu guide covers the install and the flags.

Pass a password from a script

The -P flag takes the password on the command line. The price is that the value lands in the process list and in your shell history.

unzip -P 'demo-pass-123' -p secret.zip project/app.py
print("hello from app")

For anything unattended, read the password from a file or an environment variable that is not echoed, and keep -P for a throwaway archive on your own machine.

Verify an archive before you rely on it

The test flag reads every entry through to the end and reports the outcome without writing anything to disk.

unzip -t project.zip
Archive:  project.zip
    testing: project/                 OK
    testing: project/app.py           OK
    testing: project/assets/          OK
    testing: project/assets/data.csv  OK
    testing: project/notes/           OK
    testing: project/notes/todo.txt   OK
No errors detected in compressed data of project.zip.
unzip -t project.zip reporting OK for every entry and no errors detected in compressed data
A test run reports OK per entry without writing anything to disk.

Status 0 means the data reads cleanly. I tested a truncated copy and it returned 9 with the end-of-central-directory error, which names the cause faster than an extraction attempt does.

Split and stream archives you cannot move whole

Size limits turn up on upload forms, mail servers, and removable media, and zip has an answer for each of those cases.

Split an archive into parts

The -s flag sets the size of each part, and the final part keeps the .zip name so the set stays recognisable.

zip -s 64k -r split.zip big
-rw-rw-r-- 1 ubuntu ubuntu 65536 split.z01
-rw-rw-r-- 1 ubuntu ubuntu 65536 split.z02
-rw-rw-r-- 1 ubuntu ubuntu  3741 split.zip

I hit the 64 KB floor with a 1k value, and the error names the limit instead of accepting it.

zip -s 1k -r split.zip big
zip error: Invalid command arguments (minimum split size is 64 KB:  '1k')
exit=16

Extraction needs the parts rejoined first, and the split and compress recipe covers the reassembly step.

Stream instead of writing a file

A single dash as the archive name sends the archive to standard output. You can pipe that into another program or a remote shell without a temporary file in between.

zip -q -r - project > project.zip

On the other side, -p writes a single entry to standard output, which is how you read one file out of an archive without unpacking the rest of it.

unzip -p project.zip project/notes/todo.txt
first note

For a file list you built elsewhere, -@ reads the names from standard input one per line. That is how a find result or a saved list becomes an archive contents list.

find project -name '*.txt' -print | zip -q txtfiles -@

zip is also not the only option. The tar command packs a tree with ownership intact, and gzip is the smaller choice when you only need one file to shrink.

SituationWhat to reach forWhy
An upload or mail limit caps the file sizezip -s with a part sizethe parts rejoin into one archive
The archive crosses a pipe or a remote shellzip -q -r – pathno temporary file is needed at either end
One file has to come back outunzip -p archive.zip pathonly that entry is written to standard output
The archive has to open on Windowszipthe format is supported without extra software

Every option on one page, and what to do next

The table below is the working set from this article, arranged by the task you are trying to finish.

TaskCommand
Create an archive from a filezip archive.zip file
Create an archive from a folderzip -r archive.zip folder
Zip the contents without the parent pathcd folder && zip -r ../archive.zip .
Suppress the per-file outputzip -q -r archive.zip folder
List the entriesunzip -l archive.zip
List entry names onlyunzip -Z1 archive.zip
Show method, size, and checksumunzip -v archive.zip
Extract into the current directoryunzip archive.zip
Extract into a named directoryunzip archive.zip -d target
Overwrite without promptingunzip -o archive.zip
Keep existing filesunzip -n archive.zip
Exclude entries while packingzip -r archive.zip folder -x ‘folder/skip/*’
Delete an entryzip -d archive.zip ‘folder/old.txt’
Freshen changed entries onlyzip -f archive.zip folder/file
Update and add new entrieszip -u archive.zip folder/file
Encrypt the archivezip -e archive.zip file
Test the archiveunzip -t archive.zip
Split into equal partszip -s 64k -r archive.zip folder

The one rule worth keeping is that the archive holds an entry per name you pass. A folder therefore needs the recursion flag before anything inside it is considered.

Copying the finished archive to another machine is the next step, and the scp command covers that in a single line.

For anything these tables do not answer, the man command opens the full manual that shipped with your system.

The flag descriptions here follow the packaged manual pages for zip 3.0 and unzip 6.00.

Before you send an archive anywhere, run the listing and the test. If the lengths look right and the test says OK, the file is what you think it is.

Frequently asked questions

The questions below come back most often at the prompt and in the search results.

Which flag zips a folder in Linux?

zip -r archive.zip folder. Without -r the command stores the folder name as a single zero-length entry and still exits with status 0.

Where do extracted files go?

Into the current directory by default, with the stored subdirectories recreated underneath it. Add -d target to send the whole tree somewhere else.

How do I pass a zip password in a script?

Use -P, which takes the password on the command line. It works, and it also exposes the value in the process list and in your shell history, so prefer -e when someone is there to type it.

Why does unzip ask to replace files even with -q?

The overwrite prompt is not part of the file list that quiet mode removes. Add -o to overwrite without asking, or -n to keep the files that are already on disk.