starmorph logo
Published on
· 9 min read

Symlinks Explained: How to Create and Use Symbolic Links on macOS and Linux

Symlinks Explained: How to Create and Use Symbolic Links on macOS and Linux

Symbolic links (symlinks) are one of the most useful filesystem features hiding in plain sight. They let you make a file or folder appear to exist in multiple places at once — without copying anything.

This guide covers everything from first principles to real-world workflows, with macOS-specific behavior and gotchas that will save you from data loss.

Table of Contents

A symlink is a special file that points to another file or directory. It stores a path string — that's it. When the OS encounters a symlink, it transparently follows the path to the real target.

Think of it like a shortcut, but one that works everywhere — Terminal, scripts, apps, build tools, all of it.

# The symlink itself is tiny (just a path string)
ls -la ~/.zshrc
# lrwxr-xr-x  1 dylan  staff  28 Feb 27 10:00 .zshrc -> ~/dotfiles/.zshrc

The l at the beginning of the permissions string means "link." The arrow -> shows what it points to.

macOS has three kinds of "links." They look similar but behave very differently.

FeatureSymbolic LinkHard LinkFinder Alias
StoresPath string to targetDirect reference to same inodePath + inode (hybrid)
Works on directoriesYesNo (APFS blocks it)Yes
Cross-volumeYesNo (same filesystem only)Yes
Survives target moveNo (breaks)Yes (same data)Yes (inode fallback)
Works in TerminalYesYesNo (Finder only)
Created withln -slnFinder ⌘L

Key takeaway: Symlinks are the Unix standard. They work everywhere. Use them.

Finder aliases only work in Finder and Cocoa apps. Hard links can't point to directories on APFS. Symlinks are the right tool for 99% of use cases.

The basic command:

ln -s <target> <link_name>
  • target = the real file or directory you want to point to
  • link_name = the new pointer you're creating
# Link a file
ln -s ~/dotfiles/.zshrc ~/.zshrc

# Link a directory
ln -s ~/dotfiles/nvim ~/.config/nvim

# Link into current directory (keeps the same name)
ln -s ~/Documents/notes.md .

Essential Flags

# -s  Create a symbolic link (without this, ln creates a hard link)
# -f  Force: overwrite if link_name already exists
# -n  Don't follow existing symlink-to-directory (replace it instead)
# -v  Verbose: print what was created

# The workhorse combo — idempotent, safe for scripts
ln -sfn /new/target /existing/link

The -sfn combo is what you want in setup scripts and dotfiles managers. It creates the link if it doesn't exist, replaces it if it does, and handles directory symlinks correctly.

Absolute vs Relative Paths

This is the #1 source of symlink bugs.

# ABSOLUTE — always works, regardless of where the symlink lives
ln -s /Users/dylan/dotfiles/.zshrc ~/.zshrc

# RELATIVE — path is relative to the SYMLINK's location, NOT your cwd
ln -s dotfiles/.zshrc ~/.zshrc
# Works: from ~/.zshrc's perspective, dotfiles/.zshrc is correct

# WRONG — common mistake
cd ~/dotfiles
ln -s .zshrc ~/.zshrc
# Creates ~/.zshrc -> .zshrc, which looks for ~/.zshrc (itself!) — broken!

Rule of thumb: Use absolute paths unless you have a specific reason for relative paths (like keeping symlinks portable inside a git repo that gets cloned to different locations).

The Trailing Slash Trap

This is the most dangerous gotcha when working with directory symlinks. Getting this wrong can delete your data.

# Given: my_link -> /some/directory/

# Removing a symlink — SAFE
rm my_link        # Removes the symlink only

# Removing with trailing slash — DANGEROUS
rm my_link/       # May try to remove contents of target
rm -r my_link/    # DELETES THE TARGET'S CONTENTS — DATA LOSS

# Correct way to remove a directory symlink
unlink my_link

Never use a trailing slash when removing directory symlinks. Use unlink to be explicit and safe.

Another common pitfall — updating a symlink that already points to a directory.

# This does NOT work (creates link INSIDE the target dir):
ln -sf /new/target existing_link

# This works correctly (replaces the symlink itself):
ln -sfn /new/target existing_link

The -n flag tells ln not to follow the existing symlink. Without it, ln enters the directory and creates the new link inside it instead of replacing it.

# ls -l shows the arrow notation
ls -l ~/.zshrc
# lrwxr-xr-x  1 dylan  staff  28 .zshrc -> /Users/dylan/dotfiles/.zshrc

# readlink shows the target path
readlink ~/.zshrc
# /Users/dylan/dotfiles/.zshrc

# test -L checks programmatically
test -L ~/.zshrc && echo "symlink" || echo "not a symlink"
# List all symlinks recursively
find ~/Projects -type l

# List with full details and targets
find ~/Projects -type l -ls

# Find only in current directory (not recursive)
find . -maxdepth 1 -type l

A broken (dangling) symlink is one whose target no longer exists. It still shows up in ls but resolves to nothing.

# Find broken symlinks
find ~ -maxdepth 3 -type l ! -exec test -e {} \; -print

# Clean up broken symlinks (use with caution)
find ~/Projects -type l ! -exec test -e {} \; -delete
# Preferred — explicit and safe
unlink ~/.zshrc

# Also works for file symlinks
rm ~/.zshrc

# NEVER for directory symlinks
rm -r my_link/    # This deletes the target's contents!

macOS-Specific Behavior

Finder

  • Symlinks show a curved arrow overlay on the icon (identical to Finder aliases — you can't distinguish them visually)
  • Right-click → "Show Original" (⌘R) navigates to the real target
  • Dragging a symlink to Trash removes only the link, not the target
  • Spotlight indexes the original file, not symlinks — no duplicate search results

APFS Notes

  • APFS fully supports symlinks with no special considerations
  • Firmlinks are an internal APFS concept (bridges the read-only System volume and writable Data volume since Catalina). You don't create these yourself
  • Hard links to directories are not allowed by APFS for users — only the OS uses them internally for Time Machine

macOS ships with BSD readlink, which doesn't support -f (full canonical path resolution). This trips up people coming from Linux.

# macOS built-in: resolves one level only
readlink /some/symlink

# Full resolution options on macOS
realpath /some/symlink              # Available on macOS 13+
python3 -c "import os; print(os.path.realpath('/some/link'))"

# Or install GNU coreutils
brew install coreutils
greadlink -f /some/link

SIP-Protected Paths

System Integrity Protection blocks symlink creation in certain system paths:

  • /System, /usr (except /usr/local), /bin, /sbin
  • /usr/local is writable — this is where Homebrew lives

If you need a root-level path (like /nix for the Nix package manager), use /etc/synthetic.conf:

# /etc/synthetic.conf (tab-separated, NOT spaces — reboot required)
nix	/System/Volumes/Data/nix

Real-World Use Cases

1. Dotfiles Management

The classic use case. Three popular approaches:

# Approach 1: Manual symlinks
ln -sfn ~/dotfiles/.zshrc ~/.zshrc
ln -sfn ~/dotfiles/.gitconfig ~/.gitconfig
ln -sfn ~/dotfiles/nvim ~/.config/nvim

# Approach 2: GNU Stow (automates the symlinking)
brew install stow
cd ~/dotfiles
stow zsh     # Creates ~/.zshrc -> ~/dotfiles/zsh/.zshrc
stow nvim    # Creates ~/.config/nvim -> ~/dotfiles/nvim/.config/nvim

# Approach 3: Bare git repo (no symlinks needed — files live in $HOME)
git clone --bare https://github.com/user/dotfiles ~/.dotfiles

Each has trade-offs. Stow is the most popular symlink-based approach — it computes the correct relative paths automatically. Bare git repos avoid symlinks entirely by treating $HOME as the working tree.

Symlink project directories into your Obsidian vault to search and edit everything from one place — without copying files:

# Link Claude Code plans into your vault
ln -s ~/.claude/plans ~/vault/claude-plans

# Link a monorepo's docs folder
ln -s ~/Desktop/sm-core ~/vault/sm-core

# Link standalone reference guides
ln -s ~/linux-filesystem-guide.md ~/vault/linux-filesystem-guide.md

Now your vault's graph view, search, and backlinks work across all your content. Obsidian has supported symlinks since v0.11.1 — just avoid creating loops or overlapping targets.

3. Shared Config Across Projects

Stop duplicating config files across repos:

# One Prettier config for all projects
ln -s ~/.config/prettier/.prettierrc ~/project-a/.prettierrc
ln -s ~/.config/prettier/.prettierrc ~/project-b/.prettierrc

# Shared TypeScript base config
ln -s ~/config/tsconfig.base.json ~/app/tsconfig.base.json

4. Development Shortcuts

# Quick access to deeply nested output directories
ln -s ~/Desktop/sm-core/video-production/output ~/Desktop/video-output

# Switch between SSH config profiles
ln -sfn ~/.ssh/config.work ~/.ssh/config       # Work mode
ln -sfn ~/.ssh/config.personal ~/.ssh/config   # Personal mode

# Point to a specific tool version
ln -sfn /usr/local/opt/node@18/bin/node /usr/local/bin/node

Homebrew works by installing packages in /usr/local/Cellar/ and symlinking binaries into your PATH:

# See what Homebrew has linked into your PATH
ls -la /usr/local/bin | grep "^l"

# Find broken Homebrew symlinks (stale packages)
find /usr/local/bin -type l ! -exec test -e {} \; -print

# Fix with
brew cleanup

Cloud Storage Warning

Symlinks and cloud storage don't mix well in 2026.

# This used to work with Dropbox — no longer reliable
ln -s ~/Projects ~/Dropbox/Projects

iCloud Drive, Dropbox, and Google Drive handle symlinks poorly or not at all. iCloud is especially dangerous — it can interpret symlinked content as deletions and corrupt your data. Do not rely on symlinks for cloud sync.

Quick Reference Cheatsheet

ActionCommand
Create symlinkln -s /target /link
Create or replace (idempotent)ln -sfn /target /link
Check targetreadlink /link
Check if symlinktest -L /path && echo yes
Find all symlinksfind /path -type l
Find broken symlinksfind /path -type l ! -exec test -e {} \; -print
Remove symlink safelyunlink /link

Common Mistakes Checklist

Avoid these pitfalls when working with symlinks:

  • Relative path confusion — Relative paths are relative to the link's location, not your current directory
  • Missing -n flag — Forgetting -n when overwriting a directory symlink creates the link inside the target instead of replacing it
  • Trailing slash on removalrm -r link/ deletes the target's contents, not the symlink
  • Expecting symlinks to follow moves — Moving or renaming the target breaks the symlink
  • Finder aliases in Terminal — Finder aliases (⌘L) don't work in Terminal or scripts; use ln -s instead
  • Cloud storage — iCloud Drive and Dropbox handle symlinks unreliably; avoid
  • Circular symlinksa -> b -> a creates an infinite loop; macOS stops after ~32 redirects with an ELOOP error
Share:
Enjoyed this post? Subscribe for more.