terminal-setup-install
When to auto-invoke
Auto-invoke when the user says any of:
- "set up my terminal", "install ghostty", "install oh my zsh", "set up p10k"
- "run the terminal setup guide", "install the meta-guide stack"
- "I want clickable file paths in my terminal", "install the markdown preview kit"
When NOT to invoke
Do NOT invoke for:
- Editing
~/.zshrcfor unrelated reasons (use Edit directly) - Adding individual zsh plugins (use
git clone+ Edit) - Linux or Windows terminal setup (this is macOS-specific)
- Already-installed full stacks where the user just wants to tweak one thing
What this skill does
- Preflight. Detects what's already installed and skips it.
- Core install in this exact order (sequential to avoid Homebrew portable-Ruby lock conflicts):
- Ghostty (cask)
- MesloLGS Nerd Font (cask)
- Glow (formula)
- Dracula theme for Ghostty (
git clone) - Ghostty config at
~/.config/ghostty/config - Oh My Zsh (curl installer with
RUNZSH=no KEEP_ZSHRC=no) - Powerlevel10k theme (
git cloneinto custom themes) - zsh-autosuggestions + fast-syntax-highlighting plugins
- Restore
.zshrc. OMZ overwrites~/.zshrc; the skill copies user customisations from~/.zshrc.pre-oh-my-zshback into the new file (PATH exports, aliases, tool inits). SDKMAN goes at the very end. - Set ZSH_THEME and plugins line:
ZSH_THEME="powerlevel10k/powerlevel10k"plugins=(git brew macos zsh-autosuggestions fast-syntax-highlighting)dockerplugin only ifdockeris on PATH
- AskUserQuestion about optional markdown-preview extras (multi-select).
- Per-extra installs for each chosen option.
- Sanity tests. Run
zsh -i -cchecks for parse, claude alias (if present), tool inits. - Hand off. Tell the user to open Ghostty (Spotlight) and run
p10k configureinteractively.
Step-by-step
Step 1 - Preflight
brew --version >/dev/null 2>&1 || { echo "Homebrew not installed; install it first: https://brew.sh"; exit 1; }
[ -d /Applications/Ghostty.app ] && GHOSTTY_INSTALLED=yes || GHOSTTY_INSTALLED=no
[ -d "$HOME/.oh-my-zsh" ] && OMZ_INSTALLED=yes || OMZ_INSTALLED=no
[ -d "$HOME/.oh-my-zsh/custom/themes/powerlevel10k" ] && P10K_INSTALLED=yes || P10K_INSTALLED=no
which glow >/dev/null 2>&1 && GLOW_INSTALLED=yes || GLOW_INSTALLED=no
ls "$HOME/Library/Fonts/" 2>/dev/null | grep -qi meslolgs && FONT_INSTALLED=yes || FONT_INSTALLED=no
# MacDown check - must be MacDown 3000 (the auto-refreshing fork), not the original MacDown
[ -d "/Applications/MacDown 3000.app" ] && MACDOWN3000_INSTALLED=yes || MACDOWN3000_INSTALLED=no
[ -d "/Applications/MacDown.app" ] && brew list --cask macdown >/dev/null 2>&1 && MACDOWN_ORIGINAL=yes || MACDOWN_ORIGINAL=no
Report each as a tick or "skip - already installed". If everything is already installed, skip to Step 8 (extras).
MacDown preflight rules:
- MacDown 3000 installed →
✓ MacDown 3000 - skip - Original MacDown installed but NOT MacDown 3000 →
⚠️ MacDown (original) found - will replace with MacDown 3000 in Step 10 - Neither installed →
○ MacDown 3000 - not installed
Do NOT treat the original MacDown as equivalent to MacDown 3000. The original does not auto-refresh on external file edits and uses a different bundle ID (com.uranusjr.macdown vs app.macdown.macdown3000).
Step 2 - Backup .zshrc
If ~/.zshrc exists and ~/.zshrc.pre-oh-my-zsh does not, the OMZ installer will create the backup itself. If ~/.zshrc.pre-oh-my-zsh already exists from a prior run, leave it alone; the prior backup is more authoritative than the current .zshrc.
Read the current ~/.zshrc end-to-end and capture every export PATH=, alias, eval "$(...)", and source line. These need to be merged into the new file post-install.
Step 3 - Install Ghostty
[ "$GHOSTTY_INSTALLED" = "no" ] && brew install --cask ghostty
Step 4 - Install MesloLGS Nerd Font
[ "$FONT_INSTALLED" = "no" ] && brew install --cask font-meslo-lg-nerd-font
Run sequentially after Step 3. Parallel brew install --cask calls hit a Ruby lock and one will fail with "Another brew vendor-install ruby process is already running."
Step 5 - Install Glow
[ "$GLOW_INSTALLED" = "no" ] && brew install glow
Step 6 - Configure Ghostty
mkdir -p "$HOME/.config/ghostty/themes"
TEMP=$(mktemp -d)
git clone --depth=1 https://github.com/dracula/ghostty.git "$TEMP/d"
cp "$TEMP/d/dracula" "$HOME/.config/ghostty/themes/"
rm -rf "$TEMP"
Then write ~/.config/ghostty/config (use the Write tool, not heredocs):
theme = dracula
background = #141026
background-opacity = 0.98
background-blur = true
working-directory = ~/Projects
font-family = "MesloLGS NF"
font-size = 16
font-feature = -liga
font-thicken = true
window-padding-x = 10
window-padding-y = 10
shell-integration = zsh
Critical: shell-integration must be a shell name (zsh, bash, fish), NOT true. The latter triggers a Configuration Errors dialog when Ghostty starts.
Ask the user before this step if they want a different working-directory (default ~/Projects).
Step 7 - Install Oh My Zsh + Powerlevel10k + plugins
RUNZSH=no KEEP_ZSHRC=no sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"
CUSTOM="${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}"
git clone --depth=1 https://github.com/romkatv/powerlevel10k.git "$CUSTOM/themes/powerlevel10k"
git clone --depth=1 https://github.com/zsh-users/zsh-autosuggestions.git "$CUSTOM/plugins/zsh-autosuggestions"
git clone --depth=1 https://github.com/zdharma-continuum/fast-syntax-highlighting.git "$CUSTOM/plugins/fast-syntax-highlighting"
RUNZSH=no stops the OMZ installer from spawning a child zsh that blocks scripted flows. KEEP_ZSHRC=no lets it back up and replace ~/.zshrc.
Step 8 - Restore .zshrc customisations
Edit the new ~/.zshrc to:
- Replace
ZSH_THEME="robbyrussell"withZSH_THEME="powerlevel10k/powerlevel10k". - Replace
plugins=(git)withplugins=(git brew macos zsh-autosuggestions fast-syntax-highlighting)(adddockeronly ifwhich dockerreturns a path). - After the
# Example aliasesblock, append every customisation captured in Step 2, in this order:- PATH exports (claude, .local, claude-code-docs, etc.)
- bun init block
- Other tool inits (nvm, pyenv, rbenv) EXCEPT SDKMAN
- User aliases
- At the very end of the file, append the SDKMAN init. The SDKMAN installer is explicit: it must be the last
PATH-mutating init. Place a marker comment for clarity:
# === SDKMAN MUST BE AT THE END OF THE FILE ===
export SDKMAN_DIR="$HOME/.sdkman"
[[ -s "$SDKMAN_DIR/bin/sdkman-init.sh" ]] && source "$SDKMAN_DIR/bin/sdkman-init.sh"
Step 9 - AskUserQuestion: optional extras
Use this exact AskUserQuestion (multi-select):
- Question: "Which optional markdown-preview extras would you like installed?"
- Header: "MD extras"
- multiSelect:
true - Options:
- MacDown 3000 + .md handler - Native macOS split-view markdown editor (notarised fork of MacDown that auto-refreshes when the file is changed externally). After install, double-clicking any .md in Finder opens it. Deep link:
https://www.youngleaders.tech/p/7982b16d-9aa5-4281-b370-6647134cdf7d?postPreview=paid#macdown-3000 - grip - live browser preview - Serves a GitHub-flavoured preview at localhost:6419 and auto-reloads on save. Deep link:
https://www.youngleaders.tech/p/7982b16d-9aa5-4281-b370-6647134cdf7d?postPreview=paid#grip - mdwatch - live terminal re-render - Pairs entr with glow -p so the terminal preview re-renders the moment you save. Deep link:
https://www.youngleaders.tech/p/7982b16d-9aa5-4281-b370-6647134cdf7d?postPreview=paid#mdwatch - Clickable file paths (mdls + o) - OSC 8 hyperlinks in any modern terminal; mdls lists .md files as Cmd-clickable links. Deep link:
https://www.youngleaders.tech/p/7982b16d-9aa5-4281-b370-6647134cdf7d?postPreview=paid#clickable-paths
- MacDown 3000 + .md handler - Native macOS split-view markdown editor (notarised fork of MacDown that auto-refreshes when the file is changed externally). After install, double-clicking any .md in Finder opens it. Deep link:
TODO-DEEPLINKS - post-publish task: the four URLs above are draft-preview
links and will stop working once the post is published. After publish, capture
the four section URLs (format: https://www.youngleaders.tech/i/<numeric-post-id>/<heading-slug>)
and replace the four lines above. Substack section URLs are derived from heading
text; the planned heading slugs are macdown-3000, grip, mdwatch,
clickable-paths.
Step 10 - Per-extra installs
If user picked MacDown 3000 + .md handler:
Why MacDown 3000 instead of the original MacDown: the original does not refresh its preview when the file is changed by an external process (e.g. an agent editing the file while MacDown has it open). You have to close and reopen the file to see changes. MacDown 3000 (notarised fork by Schuyler Erle, MIT, official Homebrew cask) refreshes live. Same look and feel, fixes the one limitation that matters.
The casks conflict, so uninstall the original first if it's there:
brew list --cask macdown >/dev/null 2>&1 && brew uninstall --cask macdown
brew install --cask macdown-3000
brew install duti
# Launch MacDown 3000 once so LaunchServices registers its bundle ID
open -g "/Applications/MacDown 3000.app"
sleep 2
osascript -e 'tell application "MacDown 3000" to quit' 2>/dev/null || true
# Bundle ID for MacDown 3000 (different from the original MacDown):
duti -s app.macdown.macdown3000 .md all
duti -s app.macdown.macdown3000 .markdown all
duti -x md # verify
If a phantom com.uranusjr.macdown or io.macdown.MacDown registration is
cached from an older install, rebuild LaunchServices:
/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister -r -domain local -domain system -domain user
/System/Library/Frameworks/CoreServices.framework/Versions/A/Frameworks/LaunchServices.framework/Versions/A/Support/lsregister "/Applications/MacDown 3000.app"
duti -s app.macdown.macdown3000 .md all
If user picked grip:
brew install grip
Append to ~/.zshrc:
alias preview="grip"
If user picked mdwatch:
brew install entr
Append to ~/.zshrc:
alias mdwatch='f() { echo "$1" | entr -c glow -p "$1" }; f'
If user picked Clickable file paths:
Step A - Install the OSC 8 formatter utility:
mkdir -p "$HOME/.claude/global-utils/clickable-paths"
SKILL_DIR="$(dirname "$(realpath "${BASH_SOURCE[0]}")")"
# Copy the bundled (fixed) formatter - includes Ghostty + FORCE_HYPERLINK support
cp "$SKILL_DIR/../../scripts/global-utils/format-clickable-path.js" \
"$HOME/.claude/global-utils/clickable-paths/format-clickable-path.js"
If the script location can't be determined, fall back to checking whether ~/.claude/global-utils/clickable-paths/format-clickable-path.js already exists. If absent in both cases, warn the user and offer to skip.
Step B - Install the Claude Code PostToolUse hook:
This hook makes bare filenames in Bash tool output clickable automatically - no manual mdls needed.
# Copy the hook script
cp "$SKILL_DIR/../../scripts/post-bash-filename-links.py" \
"$HOME/.claude/hooks/post-bash-filename-links.py"
Then patch ~/.claude/settings.json to wire up the hook. Use Python to read/write so JSON stays valid:
import json, os
settings_path = os.path.expanduser('~/.claude/settings.json')
# Load existing settings (create minimal structure if missing)
if os.path.exists(settings_path):
with open(settings_path) as f:
settings = json.load(f)
else:
settings = {}
settings.setdefault('hooks', {})
settings['hooks'].setdefault('PostToolUse', [])
# Find or create the Bash PostToolUse entry
bash_entry = next(
(e for e in settings['hooks']['PostToolUse'] if e.get('matcher') == 'Bash'),
None
)
if bash_entry is None:
bash_entry = {'matcher': 'Bash', 'hooks': []}
settings['hooks']['PostToolUse'].append(bash_entry)
bash_entry.setdefault('hooks', [])
new_hook = {
'type': 'command',
'command': 'python3 ~/.claude/hooks/post-bash-filename-links.py',
'timeout': 8
}
# Idempotent - don't add twice
already = any('post-bash-filename-links' in h.get('command', '')
for h in bash_entry['hooks'])
if not already:
bash_entry['hooks'].append(new_hook)
with open(settings_path, 'w') as f:
json.dump(settings, f, indent=4)
print('✓ Claude Code hook wired up in ~/.claude/settings.json')
else:
print('✓ Claude Code hook already present - skipped')
Step C - Add zsh aliases for manual use:
Append to ~/.zshrc:
mdls() {
local dir="${1:-.}"
local util="$HOME/.claude/global-utils/clickable-paths/format-clickable-path.js"
if [[ ! -f "$util" ]]; then
echo "format-clickable-path.js not found at $util" >&2
return 1
fi
for f in "$dir"/*.md(N); do
node -e "console.log(require('$util').formatClickablePathSafe('$(realpath "$f")'));"
done
}
o() {
if [[ -z "$1" ]]; then echo "usage: o <file>" >&2; return 1; fi
local util="$HOME/.claude/global-utils/clickable-paths/format-clickable-path.js"
local abs="$(realpath "$1" 2>/dev/null || echo "$1")"
if [[ -f "$util" ]]; then
node -e "console.log(require('$util').formatClickablePathSafe('$abs'));"
else
echo "$abs"
fi
open "$abs"
}
How the two approaches differ:
- Hook (Step B): automatic - every Bash tool call in Claude Code scans stdout for bare filenames and makes them clickable without any manual action
mdls/oaliases (Step C): manual - runmdls docs/workflows/oro somefile.mdexplicitly in a terminal
Step 11 - Sanity tests
zsh -i -c 'echo OK'
zsh -i -c 'type p10k >/dev/null && echo "p10k OK" || echo "p10k MISSING"'
zsh -i -c 'glow --version'
# If user has SDKMAN:
[ -d "$HOME/.sdkman" ] && zsh -i -c 'type sdk >/dev/null && echo "sdk OK" || echo "sdk MISSING"'
Step 12 - Hand off
Tell the user:
- Open Ghostty (Spotlight, type "Ghostty"). It should start in dracula theme with the Nerd Font.
- Run
p10k configure- interactive wizard for prompt style, character set, colours, icons, git status, time display.- If icons render as boxes or
?: Cmd+Q out of Ghostty and reopen. New tabs alone don't reload the font.- Optional smoke tests:
glow README.md, typegi(autosuggestions kick in),cdthen a dir name (highlighting).
Known gotchas (encoded in this skill)
| Gotcha | Encoded behaviour |
|---|---|
Parallel brew install --cask hits Ruby lock |
Installs run sequentially |
| Original MacDown does not auto-refresh on external file edits | Skill installs MacDown 3000 (notarised fork) which does |
Original macdown cask conflicts with macdown-3000 |
Skill uninstalls the original first when present |
MacDown 3000 bundle ID is app.macdown.macdown3000 (not com.uranusjr.macdown) |
Skill uses the new ID for duti |
| Phantom LaunchServices entry from older MacDown installs | Skill rebuilds LS database when needed |
shell-integration = true triggers Ghostty config error |
Skill writes shell-integration = zsh |
OMZ overwrites ~/.zshrc and loses customisations |
Skill captures customisations Step 2, restores Step 8 |
SDKMAN must be last in ~/.zshrc |
Skill places SDKMAN init at end with marker comment |
docker plugin warns when docker not installed |
Skill conditionally adds docker to plugins line |
| New tabs don't reload Ghostty font | Skill instructs user to fully Cmd+Q and reopen |
| Powerlevel10k wizard needs interactive input | Skill installs theme then hands off to user for p10k configure |
Failure modes
- Homebrew not installed: Abort with link to brew.sh.
- Already-installed core stack, just want extras: Skip Steps 3-8 entirely, jump straight to Step 9.
- Custom
~/.zshrcis too unusual to safely re-merge: Surface the captured customisations to the user as a diff and ask them to confirm before applying. - MacDown 3000 registration fails after LS rebuild: Tell user to manually open MacDown 3000 once via Spotlight, then re-run the skill from Step 10 onwards.
format-clickable-path.jsmissing for the OSC 8 option: Skip with a clear message; do not addmdls/oaliases that will error out.
See also
- The companion blog post (with screenshots): the meta-guide that taught this pattern.
- The original install PDF and FAQ PDF (April 2026 internal Toast guide; this skill encodes both their happy paths and their bugs).