Arrive with a working environment. The mob is not the place to debug your PATH.
A mob session is a shared keyboard with a rotating typist. Every person in the rotation has to be able to clone the repo, install dependencies, run the tests, run mutation testing, and hand the driver's seat to the next person without ceremony. This page walks through that setup from a clean machine on Linux, macOS, and Windows, then covers the handover tool the Guild uses, mob.sh.
Budget about thirty minutes the first time. Do it a day before the session, not five minutes before it.
Throughout this page, <repo-url> stands for the Git URL of the Guild sample repository you were given, and <repo> for the directory it clones into. The setup below is deliberately generic: it works for any Node.js and TypeScript repository the Guild mobs on.
1. Git and Repository Access
mob.sh is a thin layer over Git. If Git is not working, nothing else on this page will work either. You need Git 2.30 or newer and push access to the sample repo — mob handovers push a work-in-progress branch to the shared remote, so read-only access is not enough.
Linux
# Debian / Ubuntu
sudo apt update && sudo apt install -y git
# Fedora / RHEL
sudo dnf install -y git
# Arch
sudo pacman -S git
git --version
macOS
Nearly everything on this page installs through Homebrew on a Mac, so install that first if you do not already have it. It also pulls in the Xcode Command Line Tools, which is where macOS keeps Git and the compilers that native npm packages need.
# Homebrew — skip if `brew --version` already works
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
# on Apple Silicon, add brew to your PATH as the installer instructs
echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.zprofile
source ~/.zprofile
# Xcode Command Line Tools, if the Homebrew install did not already trigger them
xcode-select --install
# macOS ships an Apple-built Git; a current Homebrew one is better
brew install git
git --version
Windows
Install Git for Windows, which also gives you the Git Bash shell. Several commands on this page are friendliest there.
winget install --id Git.Git -e
# or with Chocolatey
choco install git
git --version
Identity and line endings
Set your identity before the session, because mob commits carry it. On a cross-platform mob, line endings cause noisy diffs unless everyone agrees, so keep the repository's .gitattributes authoritative and stop Git from rewriting endings behind your back.
git config --global user.name "Your Name"
git config --global user.email "you@example.com"
# Linux and macOS
git config --global core.autocrlf input
# Windows
git config --global core.autocrlf false
macOS has one extra Git wrinkle worth settling before the session: the default filesystem is case-insensitive, so Button.ts and button.ts are the same file to your Mac but two different files to your Linux teammates and to CI. If a rename only changes capitalization, make Git notice it:
git config --global core.ignorecase false
Authentication
Set up SSH keys or a credential helper so pushes do not prompt for a password mid-handover. Verify by cloning and pushing a throwaway branch before the session:
git clone <repo-url>
cd <repo>
git checkout -b access-check-<your-name>
git push -u origin access-check-<your-name>
git push origin --delete access-check-<your-name>
git checkout main
If that round trip works, you are cleared for handovers.
2. Node.js
Install Node through a version manager, not from a system package. Mob repos pin a Node version, members join from different machines, and you will want to switch versions without a reinstall.
Linux and macOS — nvm
The same tool covers both. Take the current install command from the nvm-sh/nvm README rather than copying a version number that ages out. It looks like this:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash
# reload your shell, then
nvm install --lts
nvm use --lts
nvm alias default lts/*
If nvm is not found after installing, your shell startup file was not reloaded. Open a new terminal, or source ~/.bashrc / ~/.zshrc.
macOS specifics: the default shell is zsh, so the installer appends to ~/.zshrc — if you keep your PATH in ~/.zprofile instead, move the nvm block there yourself. And do not install Node with brew install node alongside nvm. Two Node installations racing for the same PATH entry is a class of bug that eats a whole rotation.
Windows — nvm-windows
nvm-windows is a separate project from Linux nvm, with slightly different commands. Install it, then run the version commands from an Administrator shell — it creates a symlink, which needs elevation.
winget install --id CoreyButler.NVMforWindows -e
# in an Administrator PowerShell, after reopening the terminal
nvm install lts
nvm use lts
Respecting the repo's pinned version
If the repo has an .nvmrc, use it. That is the version the mob is running.
# Linux and macOS
nvm install && nvm use
# Windows (nvm-windows has no .nvmrc support — read it and install that version)
type .nvmrc
nvm install 22.11.0
nvm use 22.11.0
Verify and install project dependencies
node --version
npm --version
cd <repo>
npm ci
Use npm ci, not npm install. It installs exactly what the lockfile says and fails loudly if the lockfile and package.json disagree — which is what you want when the whole mob must be running identical dependencies. If the repo uses pnpm or Yarn, enable Corepack instead and let the repo's packageManager field decide:
corepack enable
3. TypeScript
Do not install TypeScript globally. A global tsc that differs from the project's version produces errors nobody else in the mob can reproduce. TypeScript belongs in the repo as a dev dependency, and you invoke it through npx or an npm script.
In a Guild repo it is already declared, so npm ci installed it. Confirm the project-local version:
npx tsc --version
npx tsc --noEmit # type-check the whole project without emitting files
If you are setting up a fresh repo during a session, this is the whole of it:
npm install --save-dev typescript @types/node
npx tsc --init
Editor note: in VS Code, run TypeScript: Select TypeScript Version and choose Use Workspace Version. Otherwise your editor lints against a different compiler than CI does, and you will chase phantom errors on the shared screen.
4. Jest
Jest is one of the two test runners you will meet in Guild repos. It is a dev dependency like everything else — there is nothing to install system-wide.
# already present in a Guild repo; this is for a fresh one
npm install --save-dev jest ts-jest @types/jest
npx ts-jest config:init
Running it:
npx jest # run everything once
npx jest --watch # re-run on change (needs a Git working tree)
npx jest path/to/file.test.ts # run one file
npx jest --coverage # with coverage report
Most repos wrap this in a script, so npm test is the command you will actually type in a session. Check package.json for the exact names before the mob starts.
On Windows, --watch mode inside WSL watching a Windows-mounted directory is unreliable. Keep the repo inside the Linux filesystem if you use WSL, or run natively on Windows.
5. Vitest
Vitest is the other runner. Its API is close enough to Jest's that the assertions read the same, but it is faster and handles TypeScript and ES modules without extra transform configuration.
# already present in a Guild repo; this is for a fresh one
npm install --save-dev vitest
npx vitest run # run everything once, then exit
npx vitest # watch mode (this is the default)
npx vitest run src/foo.test.ts
npx vitest run --coverage # needs @vitest/coverage-v8 installed
The trap worth knowing: bare vitest stays in watch mode. In CI, or when you just want a verdict, always write vitest run. A mob session that appears to hang after the tests pass is usually a watcher nobody noticed.
Configuration lives in vitest.config.ts — or in the test block of vite.config.ts if the repo is already a Vite project.
6. Stryker — Mutation Testing
Stryker is the tool that answers the question the Guild actually cares about: do these tests detect a defect, or do they just execute the code? It deliberately corrupts your source — flips a comparison, drops a statement, swaps a boolean — and reruns the suite for each change. A mutant that survives is a change to your code that no test complained about.
Coverage tells you what was executed. Mutation score tells you what was verified. That distinction is the reason it is on this page.
Install
Stryker needs a runner plugin matching the test framework the repo uses. Install the core plus one of the runners:
npm install --save-dev @stryker-mutator/core
# with Jest
npm install --save-dev @stryker-mutator/jest-runner
# with Vitest
npm install --save-dev @stryker-mutator/vitest-runner
# optional but recommended on TypeScript projects:
# discards mutants that do not even compile, which speeds up the run
npm install --save-dev @stryker-mutator/typescript-checker
On a fresh repo, the interactive initializer writes the config and picks the plugins for you:
npx stryker init
Run
npx stryker run
The run ends with a mutation score and writes an HTML report — typically reports/mutation/mutation.html — that you open in a browser to see exactly which mutants survived and where.
Expect it to be slow
Stryker runs your test suite once per mutant. On a real codebase that is minutes, not seconds. Two things make it usable in a session:
# restrict to the files the mob is working on
npx stryker run --mutate "src/checkout/**/*.ts"
# use more workers (defaults to roughly half your cores)
npx stryker run --concurrency 4
Do not launch a full mutation run in the middle of a five-minute rotation. Run it scoped while you work, and run it whole at a natural break.
Thresholds
stryker.config.json carries the thresholds that decide whether a run passes:
{
"thresholds": { "high": 80, "low": 60, "break": 60 }
}
break is the one with teeth — a score below it fails the command. The other two only colour the report.
Windows note: Stryker spawns many worker processes and writes into a temporary sandbox directory. If a run crawls or fails oddly, an on-access antivirus scanner is the usual culprit; excluding the repo directory fixes it.
macOS note: Stryker copies the project into a sandbox per worker and opens a lot of files at once. On a many-core Mac this can hit the default open-file limit and surface as EMFILE or spurious test failures. Raise the limit for the current shell, or simply run with fewer workers:
ulimit -n 4096
# or
npx stryker run --concurrency 4
7. mob.sh — The Handover Tool
mob.sh is a small command-line tool that automates the one mechanical chore in remote mob programming: moving the work-in-progress from the current typist's machine to the next one. It does this through Git, on a dedicated WIP branch, so nothing depends on screen-sharing control handoff or copying files around.
Install — Linux
curl -sL install.mob.sh | sudo sh
# or, if you use Homebrew on Linux
brew install remotemobprogramming/brew/mob
Install — macOS
Homebrew is the path of least resistance and keeps mob updated with everything else:
brew install remotemobprogramming/brew/mob
# the install script also works
curl -sL install.mob.sh | sudo sh
If you download the binary by hand instead, macOS will quarantine it and refuse to run it the first time. Clear the quarantine attribute rather than clicking through System Settings each time:
xattr -d com.apple.quarantine /usr/local/bin/mob
Install — Windows
# Scoop
scoop install mob
# Chocolatey
choco install mobsh
Without a package manager, download the Windows binary from the releases page, unzip it, and put mob.exe in a directory that is on your PATH.
Verify
mob help
If that prints usage text, mob is installed and on your PATH. If it does not, the binary landed somewhere your shell does not search — fix the PATH now rather than during the session.
Configure
Show the current configuration, including which environment variables mob honours:
mob config
Settings can go in a .mob file in your home directory (personal preferences) or in the repository (team conventions the whole mob shares). The ones that matter most:
# ~/.mob — personal
MOB_TIMER_ROOM="acg-mob" # shared timer room, so everyone sees the countdown
MOB_TIMER_ROOM_USE_WIP_BRANCH_QUALIFIER=true
# .mob in the repo — team-wide
MOB_WIP_BRANCH_PREFIX="mob/"
MOB_DONE_SQUASH=squash # squash the WIP commits when the session ends
On Windows, use Git Bash or PowerShell — both work. If you use WSL, install mob inside WSL and keep the repository on the Linux filesystem, or Git will fight you over permissions and line endings.
On macOS, the rotation timer announces itself out loud through the built-in say command, so it works with no extra setup — but it will also talk over you in a shared call. Silence it if that is not what you want:
MOB_TIMER_SOUND=false
macOS may also ask for permission the first time mob triggers a notification. Grant it once, in advance, rather than in the middle of a handover.
8. Running a Session
The whole tool is four commands. Everyone starts from the same base branch, freshly pulled.
git checkout main
git pull
Take the keyboard
mob start # create or join the WIP branch and pull the latest work
mob start 10 # same, with a 10-minute rotation timer
mob start creates a WIP branch off your current branch, or joins the existing one if the mob is already running. If you have uncommitted local changes it will refuse — that is deliberate. Deal with them, or pass --include-uncommitted-changes if you truly want them carried in.
Hand it over
mob next
This commits whatever is in the working tree as a WIP commit, pushes it, and — by default — returns you to the base branch so you are back in the mob rather than holding the keyboard. The next typist runs mob start and picks up exactly where you stopped, mid-thought and mid-line if that is where the timer caught you. Incomplete work is normal here. These are not commits meant to be reviewed; they are a conveyor belt.
End the session
mob done
This collapses the WIP branch back onto the base branch and leaves all the changes staged but uncommitted on your machine. The mob then writes one real commit message together:
git commit -m "Add checkout total calculation with mutation-tested edge cases"
git push
Other commands worth knowing
mob status # what branch am I on, is a mob running
mob timer 5 # start a timer without changing branches
mob reset # delete the WIP branch locally and on the remote
mob reset is the escape hatch when the WIP branch gets into a state nobody wants to untangle. It discards WIP work, so say it out loud before you run it.
The rule the tooling exists to serve
Llewellyn Falco's phrasing is the one the Guild uses: for an idea to go from your head into the computer, it must go through someone else's hands. The typist types; they do not decide. mob.sh only automates the rotation — the discipline is yours.
9. Pre-Session Checklist
Run this end to end on your own machine before the session. Every line should succeed without prompting you for anything.
git --version # 2.30+
node --version # matches the repo's .nvmrc
npm --version
mob help # mob is on PATH
git clone <repo-url> && cd <repo>
npm ci # dependencies install clean
npx tsc --noEmit # project type-checks
npm test # test suite passes
npx stryker run --mutate "src/**/*.ts" # mutation run completes
mob start && mob next && mob start && mob done # handover round trip works
git reset # undo the staged no-op from the dry run
Also worth having ready: a working microphone, a second monitor if you have one, and your editor's font size raised enough that the rest of the mob can read your screen when you share it.
10. Troubleshooting
Windows: "npm.ps1 cannot be loaded because running scripts is disabled"
PowerShell's execution policy is blocking npm's shim. Fix it for your user account only:
Set-ExecutionPolicy -Scope CurrentUser RemoteSigned
Windows: nvm use fails or does nothing
nvm-windows creates a symlink and needs an elevated shell. Run it from an Administrator terminal.
macOS: "mob cannot be opened because the developer cannot be verified"
Gatekeeper is blocking a binary you downloaded manually. Remove the quarantine flag with xattr -d com.apple.quarantine /usr/local/bin/mob, or install through Homebrew and avoid the problem entirely.
macOS: nvm: command not found in a new terminal
The nvm block landed in a shell startup file zsh is not reading for this kind of session. Make sure it is in ~/.zshrc, and that nothing later in the file overwrites PATH.
macOS: native npm packages fail to build
Errors mentioning node-gyp, gyp: No Xcode, or a missing compiler mean the Command Line Tools are absent or stale. Run xcode-select --install. If they are installed but still broken after a macOS upgrade, sudo xcode-select --reset usually settles it.
macOS: a rename only the Mac ignores
You renamed a file's capitalization and Git saw nothing, so CI and your Linux teammates still have the old name. Set core.ignorecase false as in section 1, or force the rename with git mv --force old.ts Old.ts.
mob start refuses: uncommitted changes
Commit them, stash them, or pass --include-uncommitted-changes. Do not fight it — the check exists so one person's stray experiment does not ride into the mob's branch unannounced.
mob next fails to push
Almost always missing push access to the remote, or expired credentials. This is why the access round trip in section 1 belongs in your preparation and not in the session.
Tests pass locally, fail for everyone else
Different Node version. Check node --version against the repo's .nvmrc, and confirm your editor is using the workspace TypeScript rather than its own bundled copy.
Vitest never exits
You ran vitest instead of vitest run. It is watching, not hanging.
Stryker takes forever
Scope it with --mutate to the files under discussion, and tune --concurrency. On Windows, exclude the repo directory from real-time antivirus scanning; on macOS, raise ulimit -n or lower the worker count if you see EMFILE.
Every tool on this page exists to remove a reason to stop. The version manager removes "it works on my machine". The test runners remove "I think this is right". Stryker removes "the tests are green, so we are fine". mob.sh removes the friction of passing the keyboard.
What is left is the part no tool can do for you: thinking out loud, in a group, while someone else types.
Ready For The Mob
Environment prepared? Read what the sessions themselves look like, and who you will be sitting with.