Introduction
ln confused me for a long time, mostly because “link” sounds like it should mean the same thing every time, but hard links and symbolic links are actually built on completely different mechanisms with different guarantees and different failure modes. Once I understood the distinction properly — using stat and inode numbers to actually see what was happening under the hood — the command stopped feeling mysterious. This guide covers both link types, when to use each, and the gotchas I’ve personally run into.
What Is the ln Command?
ln creates links between files. It’s a GNU coreutils utility:
$ ln --version
ln (GNU coreutils) 9.4
Copyright (C) 2023 Free Software Foundation, Inc.
Basic Syntax
ln [OPTION]... TARGET LINK_NAME
ln [OPTION]... TARGET
ln [OPTION]... TARGET... DIRECTORY
ln [OPTION]... -t DIRECTORY TARGET...
Hard Links vs Symbolic Links — The Core Concept
This distinction is the entire point of the command, so I want to be precise about it.
A hard link is a second directory entry pointing at the same inode as an existing file. There’s no meaningful difference between the “original” and the “link” afterward — they’re both just names pointing at identical data, permissions, and metadata. I demonstrated this earlier with stat:
$ ln src.txt hardlink.txt
$ ls -li src.txt hardlink.txt
573462 -rwxr-xr-x 2 root root 6 Jul 31 01:36 hardlink.txt
573462 -rwxr-xr-x 2 root root 6 Jul 31 01:36 src.txt
Same inode number (573462) for both. Editing either name edits the same underlying data. Deleting one just decrements the link count (visible as 2 in the permission field above); the data survives as long as at least one name remains.
Hard links have real restrictions: they can’t span filesystems (an inode is only meaningful within the filesystem that allocated it), and on most systems you can’t hard-link a directory (to prevent creating cycles in the directory tree, which would break countless tools that walk directories assuming no loops).
A symbolic link (symlink) is a completely different kind of object — it’s its own inode, of type “symlink,” whose content is simply a text string holding a path. When the kernel resolves a symlink, it reads that path and looks up whatever it points to, transparently, at access time. I created one in the same test session:
$ ln -s src.txt symlink.txt
$ ls -li symlink.txt
573467 lrwxrwxrwx 1 root root 7 Jul 31 01:36 symlink.txt -> src.txt
Notice the different inode number (573467, distinct from src.txt’s 573462), the leading l in the permission string, the size (7 bytes — the length of the string src.txt), and the -> src.txt arrow showing what it points to. Symlinks can point to anything, including nonexistent targets, directories, and paths on other filesystems. If the target is removed, the symlink becomes “dangling” — it still exists as a file, but resolving it fails.
Full List of Parameters
| Option | Long form | Description |
|---|---|---|
-s | --symbolic | Create a symbolic link instead of a hard link |
-f | --force | Remove existing destination files first |
-i | --interactive | Prompt whether to remove destinations |
-n | --no-dereference | Treat LINK_NAME as a normal file if it’s a symlink to a directory |
-b | Make a backup of each existing destination file | |
--backup[=CONTROL] | Same as -b, with control over the backup method | |
-r | --relative | Create symbolic links relative to link location |
-t | --target-directory=DIRECTORY | Specify the DIRECTORY in which to create the links |
-T | --no-target-directory | Treat LINK_NAME as a normal file always |
-v | --verbose | Print name of each linked file |
--help | Display help and exit | |
--version | Output version information and exit |
Practical Examples with Output
Creating a hard link:
$ echo "hello" > src.txt
$ ln src.txt hardlink.txt
$ cat hardlink.txt
hello
Creating a symbolic link:
$ ln -s src.txt symlink.txt
$ cat symlink.txt
hello
$ ls -l symlink.txt
lrwxrwxrwx 1 root root 7 Jul 31 01:36 symlink.txt -> src.txt
Creating a relative symlink regardless of current directory:
$ ln -sr /home/claude/test/demo/src.txt /home/claude/test/demo/a/rel_link.txt
$ ls -l /home/claude/test/demo/a/rel_link.txt
lrwxrwxrwx 1 root root 9 ... rel_link.txt -> ../src.txt
-r computed the relative path (../src.txt) automatically, which is what you generally want for links inside a project directory that might be moved or copied as a whole — an absolute-path symlink would break the moment the whole tree is relocated, while a relative one keeps working.
Symlinking a directory (something hard links can’t do):
$ ln -s /var/www/releases/v2 /var/www/current
Forcing overwrite of an existing link:
$ ln -sf src.txt symlink.txt
Linking multiple targets into a directory:
$ ln -s /usr/bin/python3 /usr/bin/python3.12 mybin/
Common Use Cases
- Symlinks: pointing a stable path (
/var/www/current) at a versioned release directory for atomic deploy switching; creating shortcuts to deeply nested config files; linking shared libraries to versioned.sofiles (libfoo.so -> libfoo.so.1.2.3); making a single binary accessible under multiple command names (e.g.,busyboxsymlinked to dozens of utility names). - Hard links: deduplicating identical files without using extra disk space (backup tools like
rsync --link-destandrsnapshotuse this heavily for space-efficient incremental backups); preserving a file under two names within the same script or build process without doubling storage.
Shell Scripting and Automation
The classic atomic-deploy pattern using a relative symlink swap:
#!/usr/bin/env bash
set -euo pipefail
RELEASE_DIR="/opt/myapp/releases/$(date +%Y%m%d_%H%M%S)"
mkdir -p "$RELEASE_DIR"
# ... deploy new code into $RELEASE_DIR ...
ln -sfn "$RELEASE_DIR" /opt/myapp/current
I use -sfn deliberately here: -s for a symlink, -f to overwrite the existing link, and -n so that if /opt/myapp/current is itself a symlink to a directory, ln replaces the symlink itself rather than (incorrectly) creating a new link inside the directory it points to.
Space-efficient backup rotation using hard links (the core trick behind classic rsync-based incremental backup scripts):
rsync -a --link-dest="/backups/$(date -d yesterday +%F)" /data/ "/backups/$(date +%F)/"
Unchanged files between backups get hard-linked rather than copied, so each daily snapshot looks complete but only consumes disk space for files that actually changed.
Real-World System Administration Workflows
- Release management:
/opt/app/currentas a symlink swapped between release directories is the standard zero-downtime deployment pattern across countless deployment tools (Capistrano, custom shell deploy scripts, etc.). - Shared library versioning: Linux’s dynamic linker relies on a chain of symlinks (
libssl.so -> libssl.so.3 -> libssl.so.3.0.13) so that applications can link against a stable name while the actual file gets updated by package upgrades. - Configuration management: tools like
stowor manual dotfile setups use symlinks to link a single source-controlled config repo into$HOME, so edits in one place take effect everywhere the symlink points. - Backup deduplication:
rsnapshotand similar tools use hard links between snapshot directories to give the illusion of many full backups while storing unchanged files only once.
Comparing ln to Related Commands
- Hard link vs symbolic link, directly: hard links share an inode (identical file, multiple names, same filesystem only, survives original deletion, can’t link directories); symlinks are separate small files containing a path (can cross filesystems, can point to directories, can dangle if the target is removed, visibly distinguishable with
ls -l). ln -svs bind mounts (mount --bind): a symlink is visible and followed by any application respecting normal path resolution; a bind mount makes a directory appear at another path at the kernel level, transparent to applications, but requires root and doesn’t persist without an/etc/fstabentry or systemd unit.lnvscp:cpduplicates data (using more disk space, and diverging independently after creation);ln(hard) shares data with no extra space and immediate mutual visibility of changes;ln -sshares nothing but a path reference.
Troubleshooting Common ln Issues
“ln: failed to create hard link: Invalid cross-device link”: you’re trying to hard link across filesystems — this is impossible by design since inodes aren’t unique across filesystems; use a symlink instead.
“ln: ‘X’: hard link not allowed for directory”: most Linux filesystems block hard links to directories entirely, for the cycle-prevention reason mentioned earlier; use a symlink for directories.
Symlink shows as broken/red in ls: the target no longer exists at the recorded path. Check with readlink -f linkname to see the fully resolved path and confirm it’s missing, then either recreate the target or repoint the symlink.
Relative symlink breaks after moving a directory: this happens when the symlink’s relative path assumed a fixed position relative to its target; use ln -sr when creating it so the tool computes the correct relative path from the link’s actual location, and be aware moving just the symlink (not both link and target together) can still break it.
Performance Considerations
Hard links have zero storage or performance overhead beyond a directory entry — reading through a hard link is exactly as fast as reading the original, because it is the original file, just under another name. Symlinks add one extra path resolution step per access (the kernel has to read the link’s target and re-resolve), which is negligible for a single link but can add measurable overhead in deeply chained symlinks resolved millions of times (e.g., dynamic library loading paths on a busy server), which is one reason distros try to keep .so symlink chains short.
Security Implications
Symlinks introduce a classic attack class called symlink attacks (or TOCTOU — time-of-check to time-of-use — race conditions): a privileged script that checks a file’s properties and then opens it, if the path is attacker-writable in between, can be tricked into following a symlink swapped in at the last moment, pointing at a sensitive target. This is why well-written system scripts running as root avoid predictable temp filenames and use safe APIs (mkstemp, O_NOFOLLOW) rather than naive path-based checks. As a practical rule, never let a root-run script write to a world-writable directory using a fixed filename it merely “hopes” isn’t already a symlink.
Best Practices
- Use symlinks for anything that needs to cross filesystems, point to directories, or represent a “current version” pointer.
- Use hard links only within the same filesystem, for genuine data deduplication (backups) or preserving a file under two names.
- Prefer
-sfnfor symlink swaps that might target directories, to avoid accidentally nesting the link inside the old target. - Use
-rfor symlinks inside portable or relocatable project trees. - Be alert to symlink-based race conditions in any script running with elevated privileges.
Compatibility Across Distributions
GNU ln behaves identically across Debian/Ubuntu, RHEL/Fedora, Arch, and openSUSE. BusyBox ln (Alpine, minimal containers) supports -s, -f, and -v but often lacks -r (relative) and -T/-n refinements — verify with ln --help before relying on those in a minimal image.
Advanced Scenarios I’ve Run Into
Chained symlinks and resolution depth: symlinks can point to other symlinks, and the kernel follows the chain until it reaches a real file or hits a configured maximum depth (ELOOP, typically 40 on Linux) and refuses further resolution — a safeguard against symlink cycles like a -> b -> a. readlink -f path (or realpath path) fully resolves any chain down to the final real path, which is invaluable for debugging deeply nested symlink setups like dynamic library versioning:
$ readlink -f /usr/lib/x86_64-linux-gnu/libssl.so
/usr/lib/x86_64-linux-gnu/libssl.so.3
Finding all symlinks pointing at a specific target, useful before removing or relocating a file that other links might depend on:
$ find / -xdev -lname '*src.txt' 2>/dev/null
Finding all hard links to a given file — since hard links share an inode but have no directory record of “siblings,” you have to search by inode number across the same filesystem:
$ inode=$(stat -c %i src.txt)
$ find / -xdev -inum "$inode" 2>/dev/null
Symlinking an entire directory tree of binaries into a personal bin directory without duplicating them, a common dotfiles pattern:
$ for f in /opt/toolkit/bin/*; do
ln -sf "$f" "$HOME/bin/$(basename "$f")"
done
Hard Links and Backup Deduplication in More Detail
The rsync --link-dest pattern deserves a closer look because it’s genuinely one of the cleverer uses of hard links in everyday system administration. Each nightly backup run creates what looks like a completely independent full snapshot directory, but files unchanged since the previous snapshot are hard-linked rather than copied — meaning a week of daily “full” backups of a mostly-static system might consume barely more disk space than a single copy, while still letting you rm -rf any single day’s snapshot independently without affecting the others (since removing one hard link just decrements the shared inode’s link count, exactly as described earlier in this guide). Tools like rsnapshot, borgbackup (though Borg uses its own deduplication rather than hard links), and countless custom shell scripts build on this exact mechanism.
Summary
The single idea that makes ln click is that hard links and symbolic links solve the “multiple names for one thing” problem at two completely different layers: hard links operate at the inode level (same file, multiple directory entries, same filesystem only), while symlinks operate at the path level (a tiny separate file holding a string, resolved fresh on each access, able to point anywhere including across filesystems or at directories). Once that distinction is second nature, choosing between ln and ln -s for any given task becomes obvious.
References
- GNU Coreutils Manual —
lninvocation: https://www.gnu.org/software/coreutils/manual/html_node/ln-invocation.html - Linux man-pages project —
ln(1): https://man7.org/linux/man-pages/man1/ln.1.html symlink(7)overview: https://man7.org/linux/man-pages/man7/symlink.7.html- Ubuntu Manpage Repository: https://manpages.ubuntu.com/manpages/noble/en/man1/ln.1.html
