Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Non-admin · new Mac (Apple Silicon)

You have no admin rights, but Homebrew works here when installed into your home directory (~/brew). Set up your terminal + font first, then let brew handle the rest — GUI apps (casks → ~/Applications, no admin) and the non-problematic formulae — and manual-tools.sh install the manual-install list (coursier, direnv, bat, awscli, tinytex) as prebuilt binaries. (Those would otherwise build from source under ~/brew’s custom prefix — slow or failing — which is why they’re manual.) Goal: maximum no-admin, no password prompts.

1. Install Homebrew (no admin)

git clone https://github.com/Homebrew/brew ~/brew

Add to ~/.zprofile, then restart your terminal:

eval "$("$HOME"/brew/bin/brew shellenv)"
export HOMEBREW_CASK_OPTS="--appdir=$HOME/Applications"   # casks -> ~/Applications, no admin

HOMEBREW_CASK_OPTS=--appdir is the key: it makes brew install --cask … drop apps into ~/Applications instead of /Applications, so no app install asks for an admin password.

2. Terminal + font first

Install iTerm2 and the FiraCode Nerd Font before anything else, then configure iTerm and do the rest of the setup from inside it:

brew install --cask iterm2 font-fira-code-nerd-font

iTerm2 configuration

(Once iTerm2 is installed — by brew cask on the admin path, or into ~/Applications by manual-tools.sh on the non-admin paths — configure it as below.)

Global options (non-admin friendly)

show tabs

hide from dock

New profile (visor, font, colors, scrollback, keybindings)

create new profile

name the profile when created

name visor profile

set visor profile to default

set color presets

change font

change window style

unlimited scrollback

z / alt_c binding + hotkey button

hotkey popup options

3. The rest via brew

curl -L -o Brewfile https://raw.githubusercontent.com/teeckoo/mac-setup/main/Brewfile
brew bundle --verbose      # casks (GUI + font) + the non-problematic formulae
rm Brewfile

brew bundle installs the GUI apps as casks (VS Code, IntelliJ, Chrome, Firefox, Bruno, Slack — into ~/Applications; re-lists iTerm2 + font, already-installed casks are skipped) and the rest of the CLI tools (gkit, rg, gum, jq, node, sdk, starship, fzf, antidote, plus containers/cloud/git tools…). All no admin.

Why not everything via brew? Your ~/brew is a custom prefix, so Homebrew can’t use bottles and builds formulae from source. Casks are unaffected (prebuilt downloads), and the “rest” formulae build fast enough — but a handful are slow or fail to build from source. Those are the manual-install list, handled next.

4. The “install manually” list (prebuilt, not brew)

curl -fsSL https://raw.githubusercontent.com/teeckoo/mac-setup/main/manual-tools.sh | bash

manual-tools.sh installs the manual-install list as prebuilt binaries (so they don’t source-build under ~/brew): coursier (cs), direnv, bat, awscli, tinytex (no brew formula), R (the CRAN build relocated into ~/.local — no admin, since the r cask prompts for one), mdbook (a Rust build that ~/brew would compile from source), and container devcolima + lima + the docker CLI (colima start --vm-type vz, no admin — colima is the Docker engine). It prints guidance for eza (no macOS binary — ls/cargo install eza), wget (use curl), and gnupg (SSH commit signing).

Shell config (~/.zprofile + ~/.zshrc)

Homebrew is on PATH already (its shellenv), so you only need the interactive block.

~/.zshrc:

# ---- System ----
ulimit -n 4096            # prevent "too many open files"
setopt AUTO_CD            # cd by typing a folder name
chpwd() { ls -C; }        # listing after each cd
export DIRENV_LOG_FORMAT=""

# ---- Completions (before plugins/SDKMAN, which call compdef) ----
autoload -Uz compinit
compinit -i

# ---- Zsh plugins (antidote) ----
source "$(brew --prefix)/opt/antidote/share/antidote/antidote.zsh"
antidote load < ~/.zsh_plugins.txt

# ---- Prompt (starship) ----
eval "$(starship init zsh)"

# ---- fzf ----
[ -f ~/.fzf.zsh ] && source ~/.fzf.zsh

# ---- direnv ----
command -v direnv >/dev/null 2>&1 && eval "$(direnv hook zsh)"

# ---- SDKMAN ----
[ -d ~/.sdkman ] && export SDKMAN_DIR="$HOME/.sdkman"
[ -d ~/.sdkman ] || export SDKMAN_DIR="$(brew --prefix sdkman-cli)/libexec"
[[ -s "$SDKMAN_DIR/bin/sdkman-init.sh" ]] && source "$SDKMAN_DIR/bin/sdkman-init.sh"

Open a new shell to auto-install the plugins.

starship prompt

Create ~/.config/starship.toml:

format = """
${env_var.OPTIONAL_PROMPT_HEADER}\
$directory $git_branch$git_status$all${env_var.OPTIONAL_PROMPT_FOOTER}
$character"""

[line_break]
disabled = false

[directory]
truncation_length = 0
truncate_to_repo = false

[env_var.OPTIONAL_PROMPT_HEADER]
variable = "OPTIONAL_PROMPT_HEADER"
# The \n is placed inside the string format so it only triggers if text exists
format = "[$env_value]($style)\n"
style = "bold blue"

[env_var.OPTIONAL_PROMPT_FOOTER]
variable = "OPTIONAL_PROMPT_FOOTER"
format = "[$env_value]($style)\n" # Newline at the START
style = "bold yellow"

(starship reads this automatically; the ~/.zshrc block below runs starship init zsh.)

Zsh plugins (antidote)

Create ~/.zsh_plugins.txt:

zsh-users/zsh-autosuggestions
zsh-users/zsh-syntax-highlighting

antidote loads this on every interactive shell (see the ~/.zshrc block). Add more plugins here as you like.

fzf (fuzzy finder)

When the fzf installer prompts, answer:

PromptAnswer
fuzzy auto-completionyes
key bindingsyes
update shell config filesno

(“no” on the last one because the ~/.zshrc block already sources ~/.fzf.zsh.)

git log pager

Keep git log on one screen (git ships with the Xcode Command Line Tools):

git config --global --replace-all core.pager "less -F -X"

Troubleshooting

Gatekeeper “cannot be opened”. macOS may quarantine a downloaded binary on first run. Clear it:

xattr -dr com.apple.quarantine ~/.local/bin

compinit prompts every shell / command not found: compdef. A leftover Homebrew site-functions dir owned by the old admin makes a bare compinit refuse to run. The ~/.zshrc block uses compinit -i (ignore insecure dirs) before the plugin/SDKMAN lines — keep that order.

npm i -g permission denied. Global installs are redirected to ~/.local (npm config set prefix ~/.local), so packages land in ~/.local/bin (already on PATH). After a major Node LTS bump, packages with native bindings may need a npm i -g reinstall to match the new ABI.

Re-running is safe (idempotent)

Both perfect-bottles.sh and manual-tools.sh re-run safely — to finish a partial install or refresh to latest. CLI binaries and vendor installers overwrite in place (no duplicates); Node wipes/reinstalls latest LTS (global packages survive); GUI apps are replaced in place (a failed download leaves the old .app); antidote/fzf are left alone if present; an unreachable source keeps its existing copy and is logged. There’s no local cache, so a refresh re-downloads (~200 MB Node, ~2 GB GUI apps).