Le Quang Lam
← All writing

What actually happens when you type a command

October 11, 2026·7 min read·software

I type commands into a terminal every day, but for a long time I couldn't have said what actually happens between pressing Enter and seeing the output. This post is the short version: three layers, and what each one does with the line you type.

Three layers, three jobs

LayerJobExamples
TerminalThe window. It takes your keystrokes and draws text on screen — nothing more.Apple Terminal, iTerm2, the panel inside VS Code
ShellA program running inside that window. It reads the line you typed, works out what it means, and gets it run.zsh, bash, fish
KernelThe core of the OS. It owns the hardware and is the only thing that can actually start a process, open a file, or change a directory.XNU on macOS, Linux

The terminal doesn't understand git status. It hands the text to the shell and renders whatever comes back. Swap iTerm2 for the VS Code panel and nothing about your commands changes, because the shell is the same program in both.

External commands vs built-ins

node --version and cd src look like the same kind of thing. They aren't:

node is a file on disk. The shell finds it, asks the kernel to start a new process for it, and waits. That process prints a version and exits. The shell is exactly as it was before.

cd is not a file the shell goes looking for. It's a built-in — code that lives inside zsh itself and runs in the shell's own process. No new process is created; the shell makes one system call and its own working directory changes. It has to work this way: a separate process could only change its own directory, never the shell's.

$PATH: where the shell looks

When a command isn't a built-in, the shell has to find a file with that name. It doesn't search the whole disk — it searches the directories listed in $PATH, left to right, and stops at the first match. That's the only reason you can type node instead of the full path to the file.

$ echo $PATH | tr ':' '\n'
/Users/lam/.nvm/versions/node/v24.13.0/bin
/Users/lam/bin
/Users/lam/.local/bin
/usr/local/bin
/opt/homebrew/bin
/usr/bin
/bin
...

That order matters. There are two copies of git on my machine, and the order of $PATH is the only thing deciding which one runs:

$ type -a git
git is /opt/homebrew/bin/git
git is /usr/bin/git

Each directory in that list is there because something put it there:

DirectoryContainsManaged by
/bin, /usr/binCore OS commands: ls, cat, zshmacOS itself
/opt/homebrew/binAnything installed with Homebrew (Apple Silicon; /usr/local/bin on Intel)Homebrew, which symlinks into it on brew install
~/bin, ~/.local/binYour own scriptsYou — they don't exist until you mkdir them, and a missing directory is silently skipped
~/.nvm/versions/node/<version>/binnode, npm, and everything from npm install -g, one folder per Node versionnvm, which puts the active version's folder at the front of $PATH
node_modules/.binCLIs from a project's dependencies: eslint, nextnpm — and it is not in $PATH

That last row is why typing eslint gives you command not found in a project that clearly has it installed. There are two ways to run it: npx eslint, or an npm script. While a script runs, npm adds node_modules/.bin to $PATH — which is why this site's package.json can simply say "lint": "eslint".

.zshrc: rebuilding the shell every time

$PATH is an environment variable, which means it belongs to a process — not to your machine. Open a new terminal tab and you get a brand-new shell process that knows nothing about the previous one. Whatever you want it to know has to be told to it again.

That's what ~/.zshrc is: a script zsh runs at the start of every interactive shell. Your export PATH=... lines, aliases, and tool setup all live there because there is nowhere else for them to persist.

So $PATH isn't stored in one place — it's assembled every time a shell starts. macOS contributes the system directories from /etc/paths, Homebrew adds its own from ~/.zprofile, and .zshrc adds the rest. nvm's setup lines run last and put its folder first, which is why node resolves to nvm's copy.

A change to .zshrc only reaches shells opened afterwards. To apply it to the one you're in:

source ~/.zshrc

source is a built-in that runs the file's lines in the current shell. Run the file as a normal script instead and it executes in a child process, sets up a lovely environment, and exits with it — the same reason cd had to be a built-in.

It's also why nvm isn't a program:

$ type nvm
nvm is a shell function from /Users/lam/.nvm/nvm.sh

nvm use 22 has to change $PATH in the shell you're sitting in. A separate process couldn't. So nvm ships as a function, and the line it asks you to add to .zshrc is what loads that function into each new shell.

Oh My Zsh and plugins

zsh on its own is fairly plain. Oh My Zsh is a framework on top of it: a collection of themes and community-written plugins, plus the plumbing to load the ones you ask for.

PathWhat it is
~/.zshrcWhere you choose: ZSH_THEME, plugins=(...), then source $ZSH/oh-my-zsh.sh to load them
~/.oh-my-zshThe framework itself — a git clone of ohmyzsh/ohmyzsh, with themes/ and plugins/ inside

Since it's a git repo, omz update is essentially a git pull.

A plugin is just a script that gets sourced into your shell — functions, aliases, and key bindings added to the current process. The ones I use:

plugins=(z colored-man-pages web-search copypath zsh-autosuggestions zsh-syntax-highlighting)
PluginWhat it doesShips with Oh My Zsh
zJumps to a directory you visit often from a fragment of its nameYes
colored-man-pagesColors man pagesYes
web-searchOpens a browser search from the terminal: google "..."Yes
copypathCopies the current directory's path to the clipboardYes
zsh-autosuggestionsSuggests the rest of a command in grey, from your history — → accepts itNo
zsh-syntax-highlightingColors the command as you type: green if it exists, red if it doesn'tNo

That red-or-green is the $PATH lookup from earlier, run on every keystroke and shown to you before you hit Enter.

Just enough Vim to edit .zshrc

Sooner or later a tool drops you into Vim — or you SSH into a box where it's the only editor. Vim is "Vi IMproved", a 1991 descendant of the vi editor from 1976, and the one thing that trips everyone up is that it has modes:

ModeKeystrokes are…Get there with
NormalCommands — move, delete, copy. This is where you start.Esc
InsertText, like any other editori

If typing produces chaos instead of letters, you're in Normal mode. Press i. That, plus five commands, is enough for a quick config edit:

TypeResult
vim ~/.zshrcOpen the file
/keyword then EnterSearch; n jumps to the next match
uUndo
:wqSave and quit
:q!Quit without saving — the escape hatch when you've made a mess

None of this is something you need every day. But the next time a command isn't found, or a tool asks for a line in your .zshrc, you'll know which of the three layers it's talking to.

References