cd is the command I use before almost every other command in a terminal session, and yet for years I only knew cd path and cd ... It wasn’t until I actually read help cd in bash that I realized how much is packed into this tiny builtin — a directory stack, environment variables tracking your history, symbolic-link-aware navigation toggles, and a scripting-friendly return-to-previous shortcut. This guide covers all of it.
What Is the cd Command?
cd stands for “change directory.” Unlike rm, mv, ls, cp, chown, and chmod, cd is not a standalone executable — it’s a shell builtin, implemented directly inside bash, zsh, dash, and every other shell. I confirmed this directly:
$ type cd
cd is a shell builtin
This is not incidental — it’s a necessity. A process cannot change the working directory of its parent process; a working directory is per-process state, so if cd were an external program, running it would change the working directory of that separate child process and then immediately exit, having no effect at all on your interactive shell. Building cd into the shell itself is the only way cd somewhere can actually affect the shell you’re typing into.
Basic Syntax (Bash)
cd [-L|-P] [dir]
cd -
How cd Works Internally
Every process on Linux has a current working directory tracked in the kernel as part of its process state (visible under /proc/<pid>/cwd as a symlink). When you run cd, the shell issues the chdir() system call (or fchdir() if operating on a file descriptor) directly on itself, updating its own working directory. That’s the entire mechanism — no external binary is involved.
Bash also maintains a small amount of extra state around cd:
$PWD— updated to the new absolute path after every successfulcd.$OLDPWD— set to the previous working directory just before the change, which is what makescd -possible.$CDPATH— an optional colon-separated list of directories bash searches when youcdto a relative name that isn’t found directly under the current directory (similar in spirit to how$PATHworks for commands).
I tested the basic navigation and the cd - shortcut directly:
$ pwd
/tmp
$ cd /home/claude
$ pwd
/home/claude
$ cd -
/tmp
$ pwd
/tmp
cd - printed the directory it switched to (/tmp) and jumped straight back to it — a shortcut for cd "$OLDPWD" that I use constantly when bouncing between two directories.
Parameters and Special Arguments
| Form | Description |
|---|---|
cd (no argument) | Changes to $HOME |
cd dir | Changes to dir (relative or absolute) |
cd - | Changes to $OLDPWD (the previous directory) and prints its path |
cd .. | Changes to the parent directory |
cd ~ | Changes to $HOME (tilde expansion, handled by the shell before cd even runs) |
cd ~user | Changes to user‘s home directory |
-L | Follow symbolic links logically (default) — .. after a symlinked path moves up the logical path, not the physical one |
-P | Use the physical directory structure, resolving symlinks before processing .. |
-e | With -P, exit with non-zero status if the current directory could not be determined successfully |
-@ | On systems that support it, present a file with extended attributes as a directory containing those attributes (rarely used) |
The -L vs -P Distinction, Demonstrated
This is the single most misunderstood part of cd, so it’s worth being concrete about it. Say /srv/current is a symlink to /srv/releases/v2, and /srv/releases/v2 has a subdirectory logs.
$ cd /srv/current/logs # logical path: /srv/current/logs
$ cd ..
With the default -L (logical) behavior, that cd .. takes you back to /srv/current — it works on the path as you typed it, symlink and all, simply removing the last component. With -P (physical), bash would first resolve /srv/current to its real target /srv/releases/v2, so cd .. from logs would land you in /srv/releases, a completely different directory. This distinction has genuinely bitten me once, in a deploy script that assumed logical navigation while running under a shell configured for physical (set -P) — the .. didn’t go where I expected.
Common Use Cases
- Everyday interactive navigation between project, log, and config directories
- Toggling quickly between two working locations using
cd - - Navigating into a user’s home directory with
cd ~usernamefor administrative tasks - Setting up
$CDPATHfor faster navigation across a fixed set of frequently used base directories
Shell Scripting and Automation
A crucial habit for scripts: always check that cd succeeded before running anything else, since a failed cd that gets silently ignored can cause a script to run destructive commands in the wrong directory:
#!/usr/bin/env bash
set -euo pipefail
cd /opt/myapp/release || { echo "Failed to cd into release dir" >&2; exit 1; }
rm -rf ./build
With set -e active, a failed cd already aborts the script — but I still write the explicit || check in critical scripts for a clearer error message, since a bare failed cd under set -e can produce a confusing generic exit.
Returning to the original directory after a temporary detour — a very common pattern in build scripts:
pushd /tmp/build_workspace > /dev/null
# ... do work ...
popd > /dev/null
pushd/popd are bash builtins that maintain a directory stack, layered on top of the same cd mechanism, letting you nest multiple “go here temporarily, then come back” operations reliably, which plain cd - can’t do beyond a single level of history.
Using $CDPATH for faster navigation (interactive convenience, set in ~/.bashrc):
export CDPATH=".:$HOME/projects:/srv"
With that set, typing cd myproject from anywhere will find $HOME/projects/myproject automatically if it’s not in the current directory, without needing the full path.
Real-World System Administration Workflows
- Deployment scripts:
cdinto a release directory before running build or install steps, always checked for success first. - Log investigation: quickly hopping with
cd -between an application’s working directory and its log directory while debugging an incident. - Backup and restore scripts: using
pushd/popdto temporarily move into a backup staging directory and reliably return afterward, even if an error occurs partway (combined withtrapto guarantee cleanup). - Multi-step batch jobs: scripts that iterate over multiple project directories,
cd-ing into each one to run a build or test step, then returning to a known base directory before moving to the next.
Comparing cd to Related Concepts
cdvspushd/popd:cdonly remembers one previous directory (via$OLDPWD, accessible withcd -);pushd/popdmaintain an actual stack, letting you nest several levels of “go here, then come back” reliably.cdvschdir()in other languages: the underlying system call is the same one bash uses internally; in scripting or programming languages, callingchdir()only affects that process’s own working directory, exactly like the “can’t be an external binary” reasoning above.cdvsfind -execfor touching files “in another directory”: you often don’t need tocdat all — many commands accept a target path directly, and reaching forcdwhen a simple path argument would do adds unnecessary state to a script.
Troubleshooting Common cd Issues
“bash: cd: dir: No such file or directory”: typo, or a relative path that doesn’t exist from the current location — pwd first to confirm where you actually are.
“bash: cd: dir: Permission denied”: you need execute (x) permission on the target directory to enter it, not just read permission — see the chmod article for the directory-specific meaning of each bit.
cd “succeeds” in a script but subsequent commands act on the wrong directory: check whether the cd was run inside a subshell (e.g., inside $(...) or a pipeline segment) — subshells have their own working directory that doesn’t propagate back to the parent shell.
cd - doesn’t go where expected: $OLDPWD only tracks the single most recent previous directory; if you’ve cded multiple times since the one you wanted, cd - won’t reach further back — use pushd/popd or dirs -v for deeper history.
Performance Considerations
cd itself is essentially free — a single chdir() syscall with negligible overhead. The performance consideration that actually matters is $CDPATH: if misconfigured with many entries or slow network-mounted directories, bash has to check each one in sequence when resolving a relative target, which can introduce a noticeable pause on directories backed by slow or unreliable network filesystems (NFS especially).
Security Implications
Because cd resolves relative paths against the current working directory, scripts that cd into a directory based on unsanitized user input are a real risk — path traversal via ../../ sequences can move a script far outside its intended working area before running subsequent destructive commands. Always validate or canonicalize (realpath) any user-influenced path before cd-ing into it in a privileged script. Also, CDPATH including . (the current directory) as an early or default entry can occasionally cause a relative cd to resolve somewhere unexpected in edge cases — worth being deliberate about $CDPATH ordering if you use it.
Best Practices
- Always check (or rely on
set -e) that acdin a script actually succeeded before proceeding. - Prefer
pushd/popdover manualcdbookkeeping when a script needs to return to its starting directory reliably, especially with multiple nested detours. - Use
cd -Pdeliberately (orset -P) when a script’s correctness depends on the physical filesystem structure rather than symlinked logical paths. - Canonicalize any user-influenced path with
realpathbeforecd-ing into it in privileged contexts. - Use
$CDPATHfor personal interactive convenience, but avoid relying on it inside portable scripts, since it changes relative-path resolution in ways that can surprise someone reading the script later.
Compatibility Across Shells
cd as described here reflects bash’s implementation; POSIX sh/dash implement the same core behavior (cd, cd -, $OLDPWD, $PWD) since it’s specified by POSIX, but lack bash-specific extras like pushd/popd and some -L/-P nuances. zsh extends cd further with features like directory stack auto-tracking and spelling correction. Since cd is a shell builtin rather than a coreutils binary, its exact behavior and available flags depend on which shell you’re using — always check help cd (bash) or your shell’s manual rather than assuming coreutils-style --long-options, which builtins generally don’t support.
Advanced Scenarios I’ve Run Into
Verifying where you actually are after a symlinked cd: because logical navigation (-L, the default) preserves the symlinked path you typed rather than the resolved physical location, pwd alone can be ambiguous about which one you’re seeing. pwd -P always prints the physical path regardless of how you got there, while plain pwd (or pwd -L) prints the logical path bash has been tracking in $PWD:
$ cd /srv/current
$ pwd
/srv/current
$ pwd -P
/srv/releases/v2
Using dirs to inspect the full directory stack built up by pushd, which goes beyond what a single $OLDPWD/cd - can express:
$ pushd /var/log > /dev/null
$ pushd /etc > /dev/null
$ dirs -v
0 /etc
1 /var/log
2 /home/claude
Each pushd added a new entry at the top of the stack; dirs -v numbers them, and cd ~2 (in bash, using the ~N directory-stack shorthand) would jump directly to entry 2 without needing repeated popd calls.
Handling cd failures gracefully in interactive functions, a pattern I use in custom shell functions that need to guarantee they return to a known state even on error:
safe_cd_and_run() {
local target="$1"
local original
original="$(pwd)"
if ! cd "$target"; then
echo "Could not enter $target" >&2
return 1
fi
# ... do work ...
cd "$original" || echo "Warning: failed to return to $original" >&2
}
Autocompletion and cd in Practice
Modern interactive shells (bash with programmable completion enabled, zsh by default) tab-complete directory names after cd, and many people rely on this so heavily that they rarely type a full path by hand. It’s worth knowing that completion is a separate layer built on top of the same chdir()-based cd builtin described throughout this guide — the completion system just helps assemble the argument bash then hands to cd, and understanding the underlying mechanics still matters the moment completion can’t help, such as constructing a path dynamically inside a script.
Summary
cd is deceptively simple on the surface and genuinely foundational underneath: it exists as a shell builtin specifically because changing a process’s working directory can only ever affect that same process, and bash layers useful state on top of the basic chdir() syscall — $PWD, $OLDPWD, cd -, and the logical/physical (-L/-P) navigation distinction. Understanding those mechanics explains both the everyday convenience of cd - and the occasional confusing edge case when symlinks, subshells, or unchecked failures are involved.
References
- GNU Bash Reference Manual — Bourne Shell Builtins (
cd): https://www.gnu.org/software/bash/manual/html_node/Bourne-Shell-Builtins.html - POSIX specification for
cd: https://pubs.opengroup.org/onlinepubs/9699919799/utilities/cd.html chdir(2)system call documentation: https://man7.org/linux/man-pages/man2/chdir.2.html- Bash Directory Stack Builtins (
pushd/popd/dirs): https://www.gnu.org/software/bash/manual/html_node/The-Directory-Stack.html