Everything in the terminal chapters works, until the day it does not. A command vanishes, a disk fills, a service refuses to start. This guide is about reading those moments instead of fighting them: six failure messages, each traced to a cause by the shortest command path that separates the candidates.
It stands on its own, but it leans on the chapters for depth. Everything here was verified on Ubuntu 26.04, Fedora 44 and Arch containers, and where the distros disagree, the text says so.
Read the error, then shrink the problem
Every failure starts as text on a screen, and the text is usually honest. The habit that separates a five-minute fix from an evening of guessing is three questions, asked in order:
- What did it say, exactly? Not “it broke”. The exact words, copied out.
- What changed since it worked? An update, a moved file, a new disk, a full one.
- What is the smallest test that still fails? One command instead of a whole workflow, one file instead of a directory.
The third question is the workhorse. “The internet is down” becomes “can I reach this address”, which becomes “does any name resolve”, and each shrink either finds the broken layer or eliminates it. The symptom sections below are that shrinking, written out.
A frozen terminal has its own small arithmetic. Ctrl+C interrupts a
running command — a ping that forgot to stop, mostly, which is why the
networking chapter teaches
ping -c so the command ends by itself. And if the screen went silent
right after Ctrl+S, nothing crashed: Ctrl+S pauses terminal output and
Ctrl+Q starts it flowing again.
The commands here are deliberately brief, because each has a chapter that teaches it properly — navigating the filesystem and files and directories first among them, then permissions, processes, packages, networking, disks, the shell, and services and logs. Six symptom sections follow, plus a logs section and a full drill at the end.
“command not found”
$ xyzzy
bash: xyzzy: command not found
$ echo $?
127
Bash has never played the adventure games that made xyzzy a magic word, so it just reports the absence. Exit code 127 is the formal statement that nothing by this name exists, and the bash guide’s exit-codes section explains the whole zero-versus-nonzero business. Ubuntu desktop installs often append a package hint to this error. It is a convenience layer, and the message means the same without it.
Three different causes produce this one message, and the shrinking questions sort them out.
It might be a typo
type is the built-in detective. It answers for real commands, shell
builtins and nonsense alike:
$ type xyzzy
bash: type: xyzzy: not found
$ type cd
cd is a shell builtin
$ type ls
ls is /usr/bin/ls
which does the narrower job of printing just the path. Trust the fact
that a path came back more than the path itself: on our Ubuntu container
which ls answered /usr/bin/ls and on Fedora 44 and Arch it answered
/usr/sbin/ls. Same program, different parking spot per distro.
It might not be installed
Each distro can tell you who ships a program. Same question, three dialects:
$ apt search traceroute
$ dnf provides '*/traceroute'
$ pacman -Fy
$ pacman -F traceroute
extra/traceroute 2.1.6-1, usr/bin/traceroute
apt search lists matching packages by name on Debian and Ubuntu, and
the package management chapter
covers that kind of searching. Finding which package owns a file is the
lesser-known half of the job, and the chapters leave it to this guide:
Fedora’s dnf provides takes a path pattern and names the package that
owns the file. Arch keeps a file database, which pacman -Fy refreshes
before pacman -F traceroute prints the line above.
It might be in the wrong directory
Scripts are the classic trap. Bash looks for bare names along PATH,
never in the directory you are standing in, so the script next to you is
invisible until you point at it:
$ script.sh
bash: script.sh: command not found
$ ./script.sh
hello-from-vfyscript
The ./ prefix means “here, explicitly”. For a program installed
somewhere unusual, the same logic applies to PATH itself:
$ echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
$ PATH=$PATH:/opt/vfytools
That is Ubuntu’s login default, with Fedora and Arch each adding their
own entries at the edges. The assignment reads left to right: the old
PATH, a colon, the new directory, and afterwards tools in
/opt/vfytools run by bare name. The shell essentials
chapter introduces $PATH
among the environment variables.
An alias can produce the same confusion from inside the shell itself: if
a command works for a colleague but not for you, it may be an alias in
their setup, and type NAME says so. The same chapter teaches aliases
with the familiar ll example.
One more look-alike to close on. If bash had answered
bash: ./script.sh: Permission denied, nothing in this section applies —
that is the next failure, and beginners mix the two up for years.
“Permission denied”
Here is that look-alike in full:
$ ./script.sh
bash: ./script.sh: Permission denied
$ echo $?
126
The exit code sorts the pair: 127 meant “no such command”, 126 means
“found it, not allowed to run it”. The file exists and is readable. The
execute bit is off, and one chmod +x script.sh later the permission
string has changed from -rw-r--r-- to -rwxr-xr-x and the script runs.
(You may see a trailing character after those bits — + on Ubuntu when
access lists are attached, . on Fedora for SELinux contexts. Ignore it
for this purpose.) The permissions
chapter teaches
the numeric and symbolic forms.
Directories complicate the picture, because their two important bits do
different jobs: r lets you list names, x lets you enter. “No
permissions, no access” is the summary everyone learns, and then the
verified split arrives. Both halves are one chmod away on a scratch
directory: chmod 644 somedir is the r-without-x case. With r but no
x:
$ cd /tmp/vfydir
bash: cd: /tmp/vfydir: Permission denied
$ ls /tmp/vfydir
vfyfile
$ ls -l /tmp/vfydir
ls: cannot access '/tmp/vfydir/vfyfile': Permission denied
You can read the names off the door but not walk in. Plain ls lists
them, ls -l needs to open each file for its details and fails per
entry. The mirror case (chmod 311 somedir takes the r away and
leaves the x) flips the behavior: cd sails straight through while
the listing dies:
ls: cannot open directory '/tmp/vfydir': Permission denied
The two bits are two questions: may I read the list, and may I pass the door.
The last permission failure is not about bits at all but about identity. Before reaching for sudo, check who the shell thinks you are:
$ id
uid=1000(user) gid=1000(user) groups=1000(user),27(sudo)
$ chown alice /tmp/vfyown.txt
chown: changing ownership of '/tmp/vfyown.txt': Operation not permitted
That chown error is what “you lack the authority” looks like when
stated plainly — ownership changes need root, which is why sudo chown
exists. (Ubuntu’s version of the error appends (os error 1).) A good
share of permission errors turn out to be wrong-user errors, and id
costs nothing to run.
“No space left on device”
The kernel does not negotiate about disk space. The error arrives for any write, from any program, and the first question is whether it is literally true:
$ df -h /mnt/vfyfs | tail -1
tmpfs 64M 64M 0 100% /mnt/vfyfs
(From the test rig: a 64M tmpfs, RAM posing as a disk, filled on
purpose.) df -h reports bytes. When Use% reads 100, the next question
is who ate them:
$ du -xh --max-depth=1 / 2>/dev/null | sort -rh | head -5
1.9G /
1.3G /usr
536M /var
4.8M /etc
64K /root
Read it in parts. du measures per directory, sort -rh sorts the
human-readable sizes biggest first (the pair the disks
chapter teaches), and
head -5 keeps the five biggest suspects. The -x pins du to one
filesystem, and 2>/dev/null throws away the permission complaints about
other people’s directories.
Sometimes df -h says there is plenty of space and writes fail anyway.
Then the filesystem’s second budget is the culprit: every file needs an
inode, an entry in the table of contents, and inodes can run out while
bytes remain:
$ df -i /mnt/vfyino | tail -1
tmpfs 100 100 0 100% /mnt/vfyino
$ touch /mnt/vfyino/overflow
touch: cannot touch '/mnt/vfyino/overflow': No space left on device
Same error text, opposite cause — on this filesystem df -h showed 0%
used. Millions of tiny files, the cache-directory kind, spend inodes
without moving the byte counter much, and df -i tells the two
shortages apart.
On a full root disk, the systemd journal is a frequent offender, and it will shrink itself on request:
$ journalctl --disk-usage
Archived and active journals take up 40M in the file system.
(40M on our Ubuntu container, 32M on Fedora, 16M on Arch — your number will differ.) Shrinking it is root territory:
$ sudo journalctl --vacuum-size=100M
Deleted archived journal /var/log/journal/…/system@….journal (70.1M).
Vacuuming done, freed 70.1M of archived journals from /var/log/journal/….
(The 70.1M came from a test rig that had grown well past its cap before
the vacuum ran — a normally capped journal frees less. Your numbers
will differ; the mechanism is the same.) Three honest limits, all
verified. Only archived journals get deleted;
the active file keeps collecting. The journal never shrinks to zero. And
under the limit, the command prints freed 0B and changes nothing.
To reproduce any of this deliberately, the filler of choice is
head -c 100M /dev/zero > filler.bin. The -c flag counts bytes, and
the suffixes are powers of 1024 — 1M is 1048576 bytes, 1G is
1073741824. The drill at the end of this guide uses it.
“The network is down”
A network is a stack of questions, and “the internet is broken” is too big to answer. Walk them in order, from the machine outward. The networking chapter teaches each command in full — here they become a bisection.
-
Is the TCP/IP machinery alive at all?
ping -c 1 localhostshould end in1 packets transmitted, 1 received, 0% packet loss, time 0ms. This never touches a cable — failure here means the problem is inside the machine. -
Does the interface have an address?
ip addr: look for your network device and aninetline beneath it. -
Is there a route out?
ip routeshould contain a line that starts withdefault:default via 10.40.33.1 dev eth0 proto dhcp src 10.40.33.227 metric 100The default line names the gateway (
via) and the device it lives on. Device names can differ between machines. Ours happened to sayeth0everywhere, but yours may sayenp3s0,wlan0, anything. -
Can we reach the world by raw address?
ping -c 3 1.1.1.1ending in3 packets transmitted, 3 received, 0% packet loss, time 2002msmeans packets leave and come back. -
Do names resolve? This is where the classic split happens:
$ dig example.com +short 104.20.23.154 172.66.147.243If the raw-address ping worked but the name fails with
ping: example.com: Temporary failure in name resolutionthen the network is fine and DNS is the patient — that failure exits with code 2.
dig @1.1.1.1 example.com +shortasks a specific resolver instead of the system default, separating “our resolver is broken” from “the upstream is”. Ifdigis not on your machine, the ping split above already names the layer. -
Is anything listening? For “the service is unreachable” complaints,
ss -tlnpshows who waits on which address:LISTEN 0 5 0.0.0.0:8822 0.0.0.0:* users:(("python3",pid=88555,fd=3)) LISTEN 0 5 127.0.0.1:8811 0.0.0.0:* users:(("python3",pid=88554,fd=3))Two test servers, one mystery solved. The first listens on
0.0.0.0, every interface, reachable from outside. The second listens on127.0.0.1, localhost only, invisible from anywhere else. A remarkable share of “it works on the machine but not remotely” tickets end at this line. -
Ping can lie. Some networks drop it while serving traffic happily, so test the real protocol instead.
curl -I example.comwants to answerHTTP/2 200: headers only, no page body.
If raw pings die partway, traceroute shows the last hop that answered —
the networking chapter owns it. And when ss says 0.0.0.0 but
outsiders get nothing, the suspect becomes a firewall. The networking
chapter sketches that landscape (ufw on Ubuntu, firewalld on Fedora) in
prose without commands, which is roughly where this guide stops too.
“The service won’t start”
Services fail more politely than disks — systemd writes everything down.
First the names, because they bite. The SSH daemon is ssh.service on
Ubuntu, where sshd.service also exists as an alias, and
sshd.service on Fedora and Arch. A good share of “unit not found”
panic is one distro’s name typed on another distro’s machine.
systemctl is-active NAME answers with a state, and inactive is a
valid one: ssh sat disabled on our Ubuntu container, switched off rather
than broken. The services and logs
chapter teaches the
systemctl basics.
systemctl --failed lists the sick units. A healthy machine answers
0 loaded units listed. A sick one names names —
● vfyfail.service loaded failed failed VFY broken unit — and closes
with 1 loaded units listed.
For the story behind a failure, status first, journal second:
× vfyfail.service - VFY broken unit
Active: failed (Result: exit-code) since Mon 2026-08-31 21:17:35 UTC; 31s ago
Status tells you that it failed. The journal tells you why:
$ journalctl -u vfyfail -n 20
vfyfail.service: Unable to locate executable '/nonexistent/vfyfail': No such file or directory
vfyfail.service: Failed at step EXEC spawning /nonexistent/vfyfail: No such file or directory
vfyfail.service: Main process exited, code=exited, status=203/EXEC
vfyfail.service: Failed with result 'exit-code'.
This unit pointed its ExecStart at a binary that does not exist, and
status=203/EXEC is systemd’s shorthand for that. The pattern
generalizes to any service: journalctl -u NAME -n 20 is where the
reason lives. The SSH hardening guide
reads the same lines for its own daemon.
One reload rule, learned the hard way. A new unit file is noticed on its own — drop it in place and start it, no ceremony needed. But after editing an existing one, systemd keeps running the stale version until you reload, and it says so right in the status output:
Warning: The unit file, source configuration file or drop-ins of vfyfail.service changed on disk. Run 'systemctl daemon-reload' to reload units.
The fix is written into the warning: run systemctl daemon-reload, then
start again. The expensive version of this mistake is editing a unit,
restarting the service, and testing the old configuration while believing
you tested the new one.
“The system is slow”
“Slow” is a feeling. Two numbers turn it into a diagnosis.
Load first. uptime prints three averages, for the last 1, 5 and 15
minutes, and nproc prints the number of cores they get spread across:
$ uptime
21:12:09 up 2 days, 13 min, 0 users, load average: 0.69, 0.84, 0.89
$ nproc
12
Compare the biggest load number with the core count — 12 here, yours will differ. Load 0.89 on twelve cores is a machine twiddling its thumbs. The same 0.89 on a four-core laptop is a normal workday, and 12.0 on either means work is standing in line.
Memory second:
$ free -h
total used free shared buff/cache available
Mem: 31Gi 30Mi 31Gi 144Ki 176Mi 31Gi
Read the available column, not free. Linux parks spare RAM in
buff/cache and hands it back the moment a program asks, so a scary
free next to a healthy available is normal, not a problem. (These
numbers come from a just-booted container; yours will look busier.)
Then find who is guilty:
$ ps aux --sort=-%mem | head
The memory hogs first, biggest at the top. top does the same thing
live — press M to sort by memory, P by CPU, q to leave — and the
processes chapter
gives it a full tour.
When one process is the problem, pgrep -x firefox prints the IDs of
processes whose name matches exactly. kill PID asks it to exit.
kill -9 PID forces the issue when nothing else works — a last resort,
as the processes chapter
explains.
Logs after the fact
The services and logs chapter
introduces journalctl with -n, -f and a first --since. Reading
yesterday’s failure wants three more filters, and they combine freely:
$ journalctl --since "1 hour ago"
$ journalctl --since today
$ journalctl -p err
$ journalctl -b
Both --since forms work as written. -p err is the one that surprises
people: it shows err-level entries and everything more severe. In our
probe run, logs written at crit appeared and logs at info stayed hidden —
it is a floor, not a filter for that level alone. -b cuts the output
to the current boot, which after a month of uptime is the difference
between two hundred lines and two hundred thousand.
You can watch the severity filter work on your own entries, because
logger writes into the journal. A plain logger "vfy no-tag message"
arrived in our capture as root[2431]: vfy no-tag message. The
bracketed tag is your username, picked for you when you do not pass -t.
Entries written at user.err and user.crit pass a -p err
reading, ordinary ones do not.
One honesty note about access. A fresh, unprivileged user on Ubuntu and Fedora sees only their own user-journal entries — no error message, just a shorter list. On Arch the same user gets
No journal files were opened due to insufficient permissions.
Membership in the right group opens the whole system journal: adm on
Ubuntu, wheel or systemd-journal on Fedora and Arch. Through sudo
you always see everything.
The disk failure, start to finish
Everything from the disk section in one transcript, run for real in a disposable container — a small tmpfs standing in for a full disk, so the failure could be triggered, measured and cleaned up without anyone’s actual drive at risk:
$ mkdir -p /mnt/vfyfs && sudo mount -t tmpfs -o size=64M tmpfs /mnt/vfyfs
$ df -h /mnt/vfyfs | tail -1
tmpfs 64M 0 64M 0% /mnt/vfyfs
$ head -c 100M /dev/zero > /mnt/vfyfs/filler.bin
head: error writing 'standard output': No space left on device
$ df -h /mnt/vfyfs | tail -1
tmpfs 64M 64M 0 100% /mnt/vfyfs
$ du -xh --max-depth=1 /mnt/vfyfs
64M /mnt/vfyfs
$ du -xh --max-depth=1 / 2>/dev/null | grep mnt
0 /mnt
$ du -h --max-depth=1 / 2>/dev/null | grep mnt
64M /mnt
$ rm /mnt/vfyfs/filler.bin && df -h /mnt/vfyfs | tail -1
tmpfs 64M 0 64M 0% /mnt/vfyfs
Walk it through. The fresh mount reports 64M available, nothing used.
head -c 100M asks for a hundred mebibytes of zero bytes, the
filesystem has 64M, and head dies at the wall with the error you now
recognize. (Ubuntu appends (os error 28) to that line — the form above
is the Fedora and Arch shape.) df confirms the wall: 64M used, 0
available, 100%.
The du pairs are the -x lesson, caught live. From inside the tmpfs,
du counts 64M. From the root with -x, the mountpoint /mnt counts
as 0, because -x refuses to cross into another filesystem. That is why
the drill uses it: the numbers stay attributable to the disk that owns
them. Drop the -x and the tmpfs’s 64M shows up under /mnt, billed to
the wrong disk.
The last line is the payoff. Remove the filler and the space comes back: 0 used, 64M available, 0%.
For trying this at home, the tmpfs route is the recommended one: run the
mount command from the transcript and you are filling RAM that poses as
a disk, so nothing real is at risk, everything in it is gone at the next
reboot, and the only price is sudo for the mount step. The no-mount
variant, where the filler points at a directory on a filesystem you
want to watch instead, is for a real disk only if you know exactly why
you are doing it.
Where you are now
One habit carries everything above: what did it say exactly, what changed
since it worked, and what is the smallest test that still fails. The
chapters hold the tool reference — processes and
system for top,
free and friends, services and
logs for systemctl and
journalctl in full.
Three links worth keeping, all in English:
The Arch Wiki page is the best long-form map of this territory, and the man pages answer the questions this guide deliberately skipped.