Skip to content
Shell & TerminalDeep Dive Published Updated 7 min readViews unavailable

ln in Shell Automation: Hard Links, Relative Symlinks, and Replacement Safety

Choose hard or symbolic links deliberately, calculate relative targets from the link directory, and replace deployment links without ambiguity.

ln creates another name for a file or a symbolic reference to a pathname. These are not interchangeable conveniences. A hard link is another directory entry for the same filesystem object; a symbolic link stores a path that the kernel resolves later. Their behavior under replacement, deletion, filesystem boundaries, permissions, and deployment rollback differs. Scripts that use ln -sfn as a generic “point this name at the new version” operation often hide important questions about target resolution and what happens when the destination already exists.

Start by defining the object identity that should remain stable. Hard links preserve access to one inode through multiple names, while symlinks provide indirection to a separately named target. A hard link does not preserve the original pathname, and a symlink does not guarantee that its referent exists. Choosing the wrong kind can turn cleanup into data loss, cause a path to resolve differently after a working-directory change, or make an atomic release switch impossible.

Without -s, ln normally creates a hard link. The new name and the source refer to the same inode on filesystems with inode semantics. Writing through either name changes the same underlying file; removing one name only removes that directory entry, and the content remains reachable while another hard link or open file descriptor exists. Metadata such as owner, mode, and timestamps belongs to the shared object rather than to an independent duplicate.

Hard links generally cannot cross filesystem boundaries because the inode belongs to one filesystem. They are also generally prohibited for directories to avoid cycles and traversal problems. Exact policy depends on the operating system and filesystem; do not assume that a failed hard-link attempt means the source is missing. If the task requires a distinct backup copy, hard-linking is the wrong operation because later writes may mutate the supposed backup. Copy-on-write reflinks are a separate filesystem feature and need separate validation.

Use hard links only when shared identity is intentional, such as deduplicated immutable package payloads within one filesystem. A cleanup process should inspect link count and object ownership before assuming a pathname uniquely owns its contents. When replacing a file through one hard-link name, writing in place modifies all names. To isolate a new version, create a new file and rename it into place, which detaches that name from the old inode while other names keep referring to it.

With ln -s, the target operand is stored as path text. Relative targets are interpreted relative to the directory containing the symlink, not relative to the shell’s current working directory at the time a later process opens the link. This is why the following layout works when the current link is used from any working directory:

mkdir -p releases/2026-10-04
ln -s releases/2026-10-04 current

Here the target is resolved from the parent directory that contains current. A target such as ../shared/config is likewise evaluated from that location. If the link is created from a different directory, the same relative target string may resolve somewhere else. GNU ln -r can compute a relative target from canonicalized locations, but it is a GNU extension; a portable script should calculate paths with a well-tested path library or use a deliberate absolute target when relocation is not required.

A symlink can be dangling by design. It can point across filesystems and can represent a directory reference, but it does not own or pin the target. Renaming, deleting, or replacing the referent can change the result of future traversals. Some operations inspect the symlink itself while others follow it; use lstat-style tools or readlink when link-object identity matters. An existence test that follows a broken symlink may report false even though the link entry exists.

Publish a version pointer safely

For immutable release directories, a symlink switch can make a single path resolve to either the old or new release. Do not delete the live link before creating its replacement: that creates a period in which readers see no name. A safer pattern creates a temporary sibling symlink and renames it over the stable name on the same filesystem:

release=2026-10-04
link=current
temporary=.current.next.$$
cleanup() {
    status=$?
    trap - EXIT
    rm -f -- "$temporary" || printf '%s\n' 'warning: could not remove temporary link' >&2
    exit "$status"
}
trap cleanup EXIT
trap 'exit 129' HUP
trap 'exit 130' INT
trap 'exit 143' TERM
ln -s "releases/$release" "$temporary"
test -x "$temporary/bin/service"
mv -f "$temporary" "$link"
trap - EXIT HUP INT TERM

The snippet assumes the release tree is already complete and the temporary name is reserved in a trusted directory. The final rename must be on the same filesystem. If several deployers can run at once, use a lock; otherwise one writer can replace another’s selected release. Verify the application path through the new link before switching, but understand that checks and later reads are separate operations. Avoid unsafe recursive cleanup based on a symlinked path.

ln -f and ln -sfn deserve special scrutiny. Destination-directory handling can interpret the final operand as a directory, and BSD/GNU option differences exist. A destination symlink to a directory can be treated as a directory or as the link itself depending on options and implementation. Use explicit path construction, a controlled parent directory, and platform-specific tests rather than copying an opaque flag bundle from an online snippet.

Before creating links, decide whether the target must already exist, whether a relative target must survive tree relocation, whether dangling references are allowed, and whether the link or referent is the object being replaced. Test spaces, leading hyphens, symlink-to-directory destinations, broken targets, cross-filesystem targets, and concurrent deployment. After creation, inspect both link text and resolution: readlink shows the stored target, while realpath or a controlled cd tests path resolution under its own rules.

Links are namespace operations, not access-control checks. A symlink can point outside an expected tree, and a hard link can expose a shared object under another name. Code that handles attacker-controlled paths needs descriptor-relative operations and race-resistant APIs rather than a sequence of shell tests and ln calls. Treat link creation as a deliberate filesystem design choice, then make cleanup and rollback obey that same choice.

Backup and retention consequences

A hard-linked “snapshot” is immutable only if every writer follows an immutability discipline. If a process opens one alias and truncates or edits the inode in place, all aliases observe the mutation. Applications that update data by writing a temporary file and renaming it tend to break the link for the updated name, which can be safer but may surprise a storage deduplicator that expected shared updates. Audit link counts and update patterns before using hard links as a backup technique.

Symbolic-link cleanup has a different hazard. Recursive copy and deletion tools vary in whether they follow a link supplied as an operand, a link encountered during traversal, or a link that leads to a directory. Never run rm -rf "$path"/* based on a path that may have become a symlink, and do not assume that a prior test -L check remains true until a later command. Prefer deletion of a known link entry itself, using a trusted parent directory and an exact name, and test behavior with a disposable fixture on each supported platform.

Relative symlinks make a tree relocatable because the target is evaluated from the link’s parent. They also couple the link to the directory layout. Moving only the link or only the target can break the reference. Absolute symlinks survive local renames differently but can point outside a relocated package or into an old release. Package formats often have their own link rules, including whether absolute paths are permitted, how links are archived, and when extraction applies metadata.

For a deployment link, record the expected target text and then resolve it in the final layout. A link that passes when created from the build directory can resolve differently after extraction under a staging root. Do not validate the target from the current working directory; validate from the directory containing the link. If the link points into an immutable release tree, confirm that cleanup does not remove that tree while it is active and that a rollback pointer is switched before garbage collection.

On systems where a link may be replaced by another process, shell ln and mv operations do not provide a portable compare-and-swap API. Lock the deployment directory or use a deployment service that serializes state changes. The correct link operation is only one piece of a protocol that also owns target creation, readiness checks, pointer switching, process reload, rollback, and old-target retention.

Related:

Sources:

Comments