tar is one of those tools you use maybe twice a year — the day you package a release and the day you unpack someone else’s. That gap is exactly long enough to forget which letters go where, and tar rewards the lapse by doing something confident and wrong instead of erroring.
Two things go wrong. The first scatters your coworker’s files all over your home directory. The second creates a file named z. Both stay in, because both are going to happen to you.
The tar bomb: an archive with no floor
Here’s the reflex. You’re inside a project, you want to zip it up, so you archive everything in sight:
$ ls
README.md app.py config.yml
$ tar czf ../project.tar.gz *
Looks fine. It even is fine, right up until someone extracts it. Look at what’s actually inside — this is the whole problem:
$ tar tf ../project.tar.gz
README.md
app.py
config.yml
There is no project/ at the top. The archive is a flat pile of files with no containing folder — no floor. tar tf lists an archive’s contents without extracting (t for “table of contents”, f for “the file is named next”), and it’s showing you three loose files.
Now watch it go off. Your coworker sends you project.tar.gz. You’ve got a downloads folder with your own stuff in it, you cd in, and you extract:
$ cd downloads
$ ls -1
existing.log
my-notes.txt
$ tar xzf ../project.tar.gz
$ ls -1
README.md
app.py
config.yml
existing.log
my-notes.txt
Their three files landed loose in your directory, tangled up with existing.log and my-notes.txt. Nothing overwrote anything this time — but change one of those loose names to README.md and it would have, silently. This is the “tar bomb”: an archive that detonates its contents into your current directory instead of a folder of its own. When it happens with a hundred files and a real project, cleanup is a git status and a lot of squinting.
The fix: look first, then archive the directory
Two habits defuse it completely.
Archive the directory, not its contents. Stand one level up and name the folder, so the folder itself becomes the top of the archive:
$ tar czf project.tar.gz project/
$ tar tf project.tar.gz
project/
project/README.md
project/app.py
project/config.yml
Everything is under project/ now. Extract that anywhere and it makes exactly one new folder — a floor for its own mess. You’ll know it worked when tar tf shows a single directory name on the first line and every other path hangs off it.
And when you’re on the receiving end, look before you leap. tar tf (or tar tzf to be explicit about gzip) costs nothing and tells you precisely where the files will land:
$ tar tzf project.tar.gz | cut -d/ -f1 | sort -u
project
One top-level entry means one new folder. More than one line means a bomb — so extract it into a folder you make on purpose with -C:
$ mkdir incoming && tar xzf suspicious.tar.gz -C incoming
Now even a flat archive is contained: whatever it scatters, it scatters inside incoming/. -C says “change to this directory first”, and it’s the seatbelt for any archive you didn’t pack yourself.
The second footgun: the letter that eats your filename
This one produces a genuinely baffling result. The flags c (create), z (gzip), and f (file) usually travel together as czf. The f is special: it means “the very next argument is the archive filename.” So the order isn’t decorative — f has to sit last, right before the name.
Slip and write -cfz instead of -czf:
$ tar -cfz project.tar.gz project/
tar: project.tar.gz: Cannot stat: No such file or directory
tar: Exiting with failure status due to previous errors
$ echo "exit=$?"
exit=2
Read what happened. f grabbed the next argument as the filename — but the next argument was z. So tar created an archive literally named z, then tried to add project.tar.gz and project/ to it. project.tar.gz didn’t exist, hence the error — but the damage is already on disk:
$ ls
project z
$ file z
z: POSIX tar archive (GNU)
There’s a file called z, it’s an uncompressed tar (the z that would have gzipped it got consumed as a filename), and your actual project.tar.gz was never created. The nonzero exit at least tells you something broke — but the misleading “Cannot stat” sends you hunting for a permissions problem that isn’t there.
The right form keeps f adjacent to the filename:
$ tar -czf project.tar.gz project/
$ echo "exit=$?"
exit=0
$ file project.tar.gz
project.tar.gz: gzip compressed data, from Unix, original size modulo 2^32 10240
The rule to memorize: f is always the last flag, and the archive name is always the next word. czf name, xzf name, tzf name — the name rides right behind the f, every time.
The whole safe pattern, tested
Here’s the shape to reach for when you package anything: archive the folder, inspect it, and prove there’s exactly one top-level entry before you send it. This block is opted into our test harness (lh:run) and runs on every build in a locked-down, no-network sandbox, so the version you’re reading is the version that passed:
```bash lh:run #!/usr/bin/env bash set -euo pipefail
cd “$(mktemp -d)” mkdir -p release/bin release/docs touch release/bin/tool release/docs/README.md release/config.yml
echo “==> archive the DIRECTORY, so it extracts into one tidy folder:” tar -czf release.tar.gz release/
echo “==> look before you leap (tar tf) — every path is under release/:” tar -tzf release.tar.gz
echo “==> extract into a clean dir and confirm it’s self-contained:” mkdir unpack && tar -xzf release.tar.gz -C unpack ls -1 unpack
the whole point: exactly one top-level entry, and it’s a directory
top=$(tar -tzf release.tar.gz | cut -d/ -f1 | sort -u) test “$top” = “release” echo “==> one top-level entry (‘$top’) — no tar bomb. done” ```
All the console output above is real, captured from tar (GNU tar) 1.35 on bash 5.2.21.
When this goes wrong
- You archived from inside the folder anyway. Sometimes you must — the files live where they live. Then don’t hand someone the bomb: extract with
-Cinto a folder you made, or repack it one level up first. The receiver’s-Chabit is the backstop for everyone else’s flat archive. tar tfon a.tar.gzcomplains it isn’t a tar. GNU tar auto-detects gzip on read, so plaintar tfworks here — but older or BSDtarmay not. Be explicit withtzf(list),xzf(extract),czf(create) whenever the archive is gzipped, and your commands port everywhere.- You extracted an archive with absolute paths. GNU tar strips the leading
/by default (and warns) so it can’t write to/etcbehind your back, but old tools and--absolute-namesdon’t.tar tffirst: if the paths start with/or contain.., extract into a throwaway-Cdirectory and never as root.
Two habits, and the twice-a-year tool stops surprising you: archive the directory so it carries its own floor, and tar tf before you extract so you can see the floor before you stand on it.