Symlinks (macOS & Ubuntu)

A symbolic link is a tiny file that says "I'm actually over there." That's it. It's how you keep one canonical copy of a thing and point everything else at it — exactly the move behind keeping Obsidian as the source of truth and symlinking config out to where tools expect it (~/.claude/CLAUDE.md, dotfiles, whatever). The command is trivial. What gets people is link direction, relative-vs-absolute paths, and the fact that macOS and Linux agree on the mechanics but disagree on the edges — BSD ln flags, Finder aliases that masquerade as symlinks, and cloud-sync engines that flatten links into broken copies.

On This Page

The one command, and the order that trips everyone

ln -s <target> <link_name>
#       ^source   ^the new thing you're creating

Read it as: "make link_name point to target." Target first, link second. Everyone reverses this once and creates a link with the wrong name pointing at nothing. Mnemonic: same argument order as cp — source, then destination.

ln -s /opt/app/config.yml ~/.config/app/config.yml   # link at ~/.config/... -> /opt/app/...

The flags that actually matter

ln -s   target link          # create a symbolic link
ln -sf  target link          # FORCE — overwrite an existing link (the daily driver)
ln -sfn target link          # force + no-deref — CRITICAL when relinking a directory (see below)
ln -sr  target link          # relative link, computed for you (GNU/Ubuntu only)

The -sfn gotcha (the one that silently nests)

If link is already a symlink pointing at a directory, plain ln -sf follows it and creates your new link inside the target directory instead of replacing the link. You end up with ~/.claude/CLAUDE.md/global-context.md nested garbage instead of a replaced link.

# WRONG when relinking an existing dir symlink — creates a link INSIDE the target
ln -sf ~/vault/new-dir ~/.config/thing

# RIGHT — -n stops it from dereferencing the existing link
ln -sfn ~/vault/new-dir ~/.config/thing

When relinking directories, always reach for -sfn. Burn this in.

Absolute vs relative — pick deliberately

ln -s /home/michal/vault/file.md  ~/.config/file.md     # absolute: breaks if you move the vault
ln -s ../vault/file.md            ~/.config/file.md      # relative: survives if both move together
ln -sr /home/michal/vault/file.md ~/.config/file.md      # GNU: let ln compute the relative path
ls -l link                   # shows  link -> target
ls -lO link                  # macOS: include file flags
readlink link                # print the immediate target (one hop)
readlink -f link             # Ubuntu/GNU: resolve the FULL chain to the final real path
realpath link                # canonical absolute path of the final target (both, modern)
stat link                    # metadata; the link itself vs target
file link                    # "symbolic link to ..."

macOS divergence: BSD readlink has no -f. To resolve a full chain on macOS use readlink -f only if you've installed GNU coreutils (brew install coreutils, then greadlink -f), or just use realpath which works on modern macOS.

# find all symlinks under a dir
find . -type l                       # every symlink
find . -type l ! -exec test -e {} \; -print   # BROKEN links (target gone)
find . -xtype l                      # GNU shortcut for broken links (Ubuntu)

# delete a symlink (removes the LINK, never the target)
rm link                              # correct
rm link/                             # WRONG — trailing slash can hit the target's contents
unlink link                          # explicit, single-link only

Never put a trailing slash when rm-ing a directory symlinkrm mylink/ can operate on what it points to, not the link. rm mylink (no slash) removes just the link.

ln target link               # NO -s = hard link

A hard link is a second name for the same inode — same actual data, no "pointer" file. Differences that matter:

For config/dotfile/vault work you want symbolic links 99% of the time. Hard links are for dedup and specific backup tricks (rsync --link-dest).

macOS-specific landmines

Ubuntu-specific notes

Common real-world patterns

# dotfiles: canonical copy in a git repo, linked into $HOME
ln -sf ~/dotfiles/.zshrc ~/.zshrc
ln -sf ~/dotfiles/.gitconfig ~/.gitconfig

# vault is the source of truth, render OUT to where tools expect it
ln -sf ~/Obsidian-Master/claude-brain/global-context.md ~/.claude/CLAUDE.md

# relink a directory safely (the -n is mandatory here)
ln -sfn ~/Obsidian-Master/claude-brain/projects ~/.config/claude/projects

# point "current" at a versioned release dir, flip atomically on deploy
ln -sfn /opt/app/releases/v1.2.3 /opt/app/current

That last one — a current symlink flipped between versioned release directories — is the cleanest zero-downtime deploy trick there is. Build the new release alongside, then ln -sfn to flip; the switch is atomic.

Things that bite you