01The question
Most of what sits in /usr/local/bin is compiled programs, files only a processor can read. Some of it isn't. Some files there are text you can read with cat, and they run all the same. Those are scripts, and they raise a run of questions: what makes a text file runnable, what reads it, and why there are two kinds of program at all.
The same habit of asking then turns to git. Coding agents are said to work "in their own worktree", which only makes sense once branch, commit and checkout are taken apart too. This note builds on How does my terminal find a tool?, which covers how the shell finds a command, and on What the engine does: translation, which covers compiled and interpreted code.
02A file that names its reader
A is a text file of commands. The processor never runs it. Another program, an , reads it and carries out each command. A compiled program is , which the processor runs directly. That is why cat shows every line of a script and shows garbage for node.
The file command tells the two kinds apart. On a Mac, file $(which node) says Mach-O 64-bit executable arm64, and file $(which npm) says a /usr/bin/env node script text executable. Both sit on , and the shell treats them the same way.
Two things make a text file runnable. The first is the execute bit, a permission on the file that says it may be run. chmod +x deploy.sh sets it. Without it the shell stops at permission denied. The second is the first line: #! and then a path, the . When a program is started, the looks at the file's first bytes. If they are machine code in a format it knows, it loads them. If they are #!/bin/bash, it starts /bin/bash instead and hands it the script's path. #!/usr/bin/env node starts env, which looks node up on PATH, so the script runs on whichever Node comes first.
- First line
- #!/usr/bin/env node
- Execute bit
- Set
- The kernel
- Starts /usr/bin/env, handing it node and this file's path. env finds node on PATH.
A file with no first line is refused by the kernel with exec format error. Shells catch that and run the file as a shell script themselves: zsh hands it to /bin/sh, and Bash runs it on its own. So a bare list of commands works when you type its name in a terminal, and fails when any other program tries to start it.
Inside, a shell script holds what you would type: commands in order, variables (NAME="world", then $NAME), and if, for and while, which turn the list into a program. ~/.zshrc (Where PATH comes from) is a script with neither a first line nor the execute bit, because nothing starts it. zsh reads it in, line by line, every time a shell opens.
A script is text that names its reader. The kernel reads the first line, and the program named there reads the rest.
03Where scripts came from
Unix, at Bell Labs from 1969, was built from small programs that each did one thing and could be chained together. Its first shell, Ken Thompson's, read a line you typed and ran it. It could just as well read the lines from a file, so sh file ran a saved list of commands. Those were the first shell scripts, and they were made for the plainest reason there is: anything you type every day, you could type once.
- 1979. Stephen Bourne's shell replaced Thompson's in the Seventh Edition of Unix, under the same name,
sh. Thompson's hadifandgoto, but as separate programs. Bourne put variables andif,forandwhileinside the shell, and scripts became programs in their own right. - 1980. Dennis Ritchie changed the kernel so that a file starting with
#!names its own interpreter. He thought the idea may have come from Berkeley, and most people first met it in BSD Unix. - After that, every interpreter came in through the same door: Perl, Python, Ruby, Node. A JavaScript file with
#!/usr/bin/env nodeat the top and the execute bit set runs like any other command. That is how npm can be a script.
Scripts lasted because they are glue. You don't need a new compiled program to chain together a few tools that already exist. The scripts in a package.json share the name and the idea: each is one line, which npm run hands to /bin/sh.
04Which shell
A is a program like any other, so a machine can have several. A Mac lists the ones allowed as login shells in /etc/shells: bash, csh, dash, ksh, sh, tcsh and zsh, all in /bin. Homebrew can add more, such as fish or a newer Bash.
Two of them aren't what their names suggest. The Bash Apple ships is version 3.2, from 2006, the last one under the GPLv2 licence. Later versions moved to GPLv3, and Apple stopped there. That is the usual explanation for why zsh became the default in 2019, though Apple never gave one. And on a Mac, /bin/sh isn't Bourne's shell. It is a small program that runs Bash in its stricter POSIX mode. /private/var/select/sh is a link that says which shell it runs.
The kernel has no say in which shell you use. The question has two separate answers:
- The shell a terminal opens with is your login shell, one setting per account.
chsh -s /bin/zshchanges it, and only to a shell listed in/etc/shells. A Mac keeps the setting in its directory service (dscl . -read ~ UserShell), not in/etc/passwd. - The shell that runs a script is set by the script's first line, file by file. A
#!/bin/bashscript runs under Bash whatever shell your terminal opened with.
Any shell can also start another. Type bash in zsh and you are in a child , a Bash inside the zsh. exit takes you back. Nobody has to give permission.
05Which came first
A processor runs only machine code, so something has to translate everything else, and What the engine does: translation sets out the two times it can happen: before the program ships, or while it runs. Which of the two came first? Interpreting did, narrowly. Compiling often gets the credit.
- 1949. Assemblers on machines such as EDSAC turned short written codes into machine code: a program that translates another program.
- 1950. John Mauchly's Short Code ran on UNIVAC I. Equations were written as codes, and an interpreter carried them out about fifty times slower than machine code. It is often called the first high-level language.
- 1957. FORTRAN, from John Backus's team at IBM, was the first compiler good enough that people believed compiled code could run nearly as fast as code written by hand. Backus had built an interpreter first, Speedcoding, in 1953, and FORTRAN was his way out of its slowness.
- 1958. Lisp. John McCarthy defined
eval, Lisp described in Lisp, as a piece of theory. Steve Russell, his graduate student, translated it by hand into IBM 704 machine code and had an interpreter. McCarthy had told himevalwas for reading, not for running.
So neither came first by much, and neither replaced the other. They trade off. Compiled code runs faster, but it has to be built again for each kind of processor and rebuilt after every change. Interpreted code runs as soon as you save it, on any machine that has the interpreter. That is why one machine holds both: node and git compiled once by the people who make them, and the scripts and JavaScript you write, read fresh every time they run.
06Inside an engine
A JavaScript is itself a compiled program, written in C++. It doesn't simply interpret. It works in tiers:
- Parse. Read the text into a tree of what it says.
- Interpret. Turn the tree into and run it straight away. Starting fast matters more than running fast here, because most code runs only a handful of times.
- Watch. Count what runs often and note the types it sees. Does
addalways get two numbers? - Compile what's hot. Hand the busy functions to a compiler that turns them into machine code, on the assumption that the types stay as they were ().
- Fall back. When an assumption breaks, say a string turns up where only numbers came before, throw the machine code away and go back to bytecode. That is a , and it is one reason the same JavaScript can be fast in one run and slow in another.
- Ignition
- Sparkplug
- Maglev
- TurboFan
add(a, b) is parsed into bytecode and interpreted at once. Most functions never leave this tier.
V8 and JavaScriptCore do the same job with separate code from separate teams. V8, Google's, has four tiers: Ignition interprets, Sparkplug compiles quickly without optimising, Maglev optimises a little, and TurboFan optimises hard. JavaScriptCore, Apple's, has four as well: LLInt, Baseline, DFG and FTL. They differ more in how they lay objects out in memory and how they collect garbage. That is why a program that leaks or stalls on one engine may run cleanly on the other.
"Runtime" means two things at once. It is the whole program you run (node, bun), and it is also the part of that program round the engine that reaches files and the network. Those are one thing seen at two levels of zoom, and Three layers calls them engine and harness. In Node that outer part is C++ and C, with much of its API written in JavaScript. In Bun it is mostly Zig, round Apple's C++ engine (The engine underneath). Bun also runs TypeScript by stripping out the types without checking them, so tsc --noEmit is still what finds type errors.
An engine starts out by reading your code and ends up compiling it, one function at a time, as it learns which parts are worth the effort.
07A session that outlives its window
The terminal window isn't the shell. The Terminal app draws text and passes on your keys. The shell is a separate process it starts (Typing a name). When the window closes, the shell is sent a hangup signal and ends, and the programs it started end with it. Over SSH, a dropped connection does the same thing to the shell on the far machine, and a three-hour import dies halfway.
tmux, a terminal , puts something in between. It runs a server process that holds the shells and belongs to no window. Your window is only a client looking in. Close the window or lose the connection, and the server and everything in it keep running. Run tmux attach from a new window, or after a new SSH login, and everything is as you left it, scrollback included.
- Terminal windowOpen
- zshRunning
- npm run buildRunning
Splitting the screen into panes is the feature people see, but the server is the point. Inside tmux, a session is a workspace. It holds windows, which work like tabs, and each window splits into panes, each one a shell.
- 1987. GNU Screen, by Oliver Laumann and Carsten Bormann at TU Berlin, was the first widely used multiplexer. Detaching came in a later version. A Mac still ships it, as
/usr/bin/screen. - 2007. tmux, by Nicholas Marriott, a long-time Screen user who wrote a replacement from scratch under a BSD-style licence. It went into OpenBSD. On a Mac it comes from Homebrew.
On a single laptop it matters less, and tabs cover most of it. Terminal's own split shows the same shell twice, while iTerm2 and Ghostty split into separate shells. Closing the lid doesn't favour tmux, because sleep pauses every process alike and they all carry on when the machine wakes. Locally, tmux earns its place with a long job that has to survive the window closing or the terminal app crashing. On a remote machine it earns it from the first login.
Without tmux, the window owns the shell. With it, the window only looks at a shell the server owns.
08A commit is a snapshot
Now git. A repository is a folder with a hidden .git folder inside it. The folder holds the files you edit, and .git holds the history. That history is made of commits. A is a frozen record of every file at one moment, plus who made it, when, a message, and the commit that came before it, its parent. Its name is a hash of all of that, like 15fcd2c…, so a commit can never change. Different contents would give a different name.
A snapshot isn't a copy, though. Git stores each distinct version of a file once, and a commit that leaves a file alone points at the same stored version as its parent. git cat-file -p HEAD prints the newest commit: a tree (the files), a parent, an author, a committer and the message. That is all a commit is.
09A branch is a name
A is a name that points at one commit. It isn't a copy and it isn't a folder. It is a file in .git/refs/heads/ holding the 40 characters of a commit's hash. git branch dark-mode makes one by writing that one small file.
Another small file, .git/HEAD, says which branch you are on: ref: refs/heads/main. Committing does two things. It writes a new commit whose parent is the one your branch names, then moves your branch to point at it. Only the branch HEAD names ever moves. When people say two branches have diverged, they mean exactly this: two names pointing at different commits, each with commits the other lacks.
goes the other way, from the history into the folder. Git rewrites the files in your folder to match the commit a branch names, and points HEAD at that branch. git switch dark-mode does it. git switch arrived in 2019 to take this job over from git checkout, which also restores files and was easy to misread. Edits you haven't committed come with you if they don't clash with the new files. If switching would overwrite them, git refuses.
- first page
- add header
- fix typo
- first page
- add header
- fix typo
So with no worktree, there is one folder, and branches take turns with it. Working on dark-mode changes the files in that folder, not main. The commits main points to sit untouched in .git, and switching back writes them into the folder again.
A commit is the thing itself. A branch is a name for the newest commit on a line. Checking out writes that commit into your one folder.
10A worktree is a second folder
One folder can show only one commit at a time. A is a second folder from the same repository, with a different branch checked out, so both are on disk at once. git worktree add ../myproject-dark dark-mode makes one. There is still only one history. In the new folder, .git is a file of one line pointing back into the first folder's .git.
Committing works the same way in either folder. It moves whichever branch is checked out in the folder you are in, so there's no such thing as committing "a worktree". Git also refuses to check out one branch in two worktrees at once, so two folders can never move the same name.
That is why coding agents are given a worktree each. An agent working on a branch in your folder would switch the folder under you, rewriting the files you have open. With a folder of its own, it can edit, install and break things while yours stays as it was. A new worktree only has the files git tracks, though, so each one needs its own node_modules and its own install.
- myproject/on mainYour edit, not committed
You're in myproject/ on main, halfway through an edit to header.tsx.
The branch is which line of history. The worktree is where its files sit on disk.
11Branch off, merge back
Day to day it goes like this. main is the version that works, often the one that is live. A feature starts as a branch off it, git switch -c dark-mode, and commits pile up on the branch over days while main stays where it was. When the feature is done, git switch main and git merge dark-mode bring it back.
- If `main` hasn't moved, git slides
mainforward to wheredark-modeis. That is a fast-forward, and it makes no new commit. - If it has, because someone fixed a bug on
mainin the meantime, git makes a merge commit with two parents, one from each line, holding both sets of changes. When both lines changed the same lines of a file, git stops and asks you to choose. That is a conflict.
Afterwards the branch's name is deleted with git branch -d dark-mode. Its commits stay, now reachable from main. Teams usually do the merge on a site such as GitHub, through a pull request, so that someone reviews the change first. Underneath, it is the same merge.
12Back to the script
Every piece came out the same shape. A script is text. The kernel reads its first line, and the program named there does the work. A terminal window is a view, and the shell is a process the window may or may not own. A project folder is a checkout, the history is in .git, and a branch is a name in there.
The file, the window and the folder are each the end you can see of something kept somewhere else.
Connected
- Note · same thingSuggestedHow does my terminal find a tool?
That note finds the file on PATH. This one opens it: the execute bit, the first line, and which shell reads it.
- Note · same thingSuggestedWhat actually runs my code?
Compiled and interpreted again, with the history of which came first, and the tiers inside the engine that note names.
- Note · same thingSuggestedWhat is actually in Bun?
npm being a script Node runs is the shebang at work; the engine and runtime split is the same one.
- Note · same thingSuggestedWhat happens when a program runs?
That note has the kernel starting a program. The shebang is the branch in that path where the kernel starts a different program instead.
Terms
Checked
What the chat had wrong or only half right, against sources. The note above already says the corrected thing.
- Compiled came first, by about a year: FORTRAN in 1957, then Lisp's interpreter in 1958.Interpreting came first. Short Code ran through an interpreter on UNIVAC I in 1950, and Backus's own Speedcoding (1953) was interpreted before he led FORTRAN.Wikipedia, Short Code
- The Bourne shell arrived in 1977.Bourne began it around 1975. It was released in 1979, as
shin the Seventh Edition of Unix.Wikipedia, Bourne shell shon a Mac is the original Bourne shell, kept as a fallback.On a Mac,/bin/shruns Bash in POSIX mode./private/var/select/shis a link to/bin/bash.A Mac running macOS 26, read on 23 October 2026npm run buildis literally a script that calls next, webpack and tsc.It is one line inpackage.json'sscripts, which npm runs with/bin/sh. Thenextit calls is itself a script, innode_modules/.bin.npm docs, npm run- V8 has a simpler two-tier setup than JavaScriptCore, and optimises more aggressively, sooner.V8 has four tiers too: Ignition, Sparkplug (2021), Maglev (2023) and TurboFan.V8 blog, Maglev
- JavaScriptCore was called SquirrelFish, dating back to 2008.SquirrelFish was the 2008 rewrite of JavaScriptCore's interpreter. JavaScriptCore itself began in 2002, when Apple took KDE's KJS engine for Safari.WebKit blog, Announcing SquirrelFish
- Both of Node's layers are C++, end to end.V8 is C++. Round it, Node has C++ bindings, libuv for its event loop (in C), and much of its API in JavaScript, in
lib/.Node.js source - Bun does the job of tsc, along with node, npm, webpack and jest.Bun runs TypeScript by stripping the types, and checks none of them. Type checking still needs
tsc.Bun docs, TypeScript - A tmux session survives a laptop's sleep more reliably than a terminal tab.Sleep pauses every process alike, tmux's server included, and all of them carry on at wake. tmux protects against the window or the connection going away.tmux manual
- Terminal.app already has split panes, covering most of what tmux gives you locally.Terminal's split (Command-D) shows two views of the same shell. Separate shells side by side need tabs, windows, or another terminal app.O'Reilly, Mac OS X Lion Pocket Guide: Terminal tips
- Switching branches in a shared folder means your uncommitted edits could get stashed.Git never stashes on its own. Uncommitted edits come with you when they don't clash, and git refuses to switch when they would be overwritten.Git docs, git switch