// Prerequisites
Node.js 20 LTS
All three AI CLI tools require Node.js version 18 or higher. We recommend installing Node.js 20 LTS for maximum compatibility.
Git
Git is required for version control features. All three tools integrate with Git for tracking changes, creating commits, and managing branches.
API keys
Each tool requires an API key from its respective provider. You'll set these up during installation.
// Mac Setup
macOS comes with Terminal.app, but there are better alternatives for AI coding:
Modern terminal with built-in AI assistance. Great for learning commands.
warp.devHighly customizable, split panes, search, and extensive features.
iterm2.comWorks fine for basic use. No installation needed.
Homebrew is the easiest way to install developer tools on Mac. Open Terminal and run:
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
After installation, you MUST add Homebrew to your PATH:
echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> ~/.zshrc
source ~/.zshrc
Verify installation: brew --version
nvm (Node Version Manager) lets you install and switch between Node.js versions easily.
Step 1: Install nvm
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.1/install.sh | bash
Step 2: Add to your shell profile
Add these lines to your ~/.zshrc file:
export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh"
Step 3: Restart terminal and install Node
# Restart your terminal, then:
nvm install 20 # Install Node.js 20 LTS
nvm use 20 # Use Node.js 20
node --version # Verify installation
macOS may already have Git installed. Check first:
git --version
If not installed, use Homebrew:
brew install git
First time? Configure your identity:
git config --global user.name "Your Name"
git config --global user.email "your@email.com"
// Windows Setup
WSL2 gives you a full Linux environment inside Windows. Most programming tutorials assume Unix-like systems, so this makes following them much easier.
Modern Windows shell. Works for all three CLI tools but some commands differ from tutorials.
Old Windows shell. Not recommended for development work.
Get Windows Terminal: Free from the Microsoft Store. It lets you use PowerShell, CMD, and WSL2 in one app with tabs.
Step 1: Open PowerShell as Administrator
Right-click the Start menu and select "Windows Terminal (Admin)" or "PowerShell (Admin)"
Step 2: Install WSL
wsl --install
This installs Ubuntu by default. You'll be prompted to restart.
Step 3: Set up your Linux user
After restart, Ubuntu will open and ask you to create a username and password. This is separate from your Windows login.
After WSL2 is set up: Use Windows Terminal to open an "Ubuntu" tab. All following Node.js and Git commands will work just like on Mac/Linux.
Enable Script Execution
Windows blocks scripts by default. Open PowerShell as Administrator and run:
Set-ExecutionPolicy RemoteSigned
Install Node.js with nvm-windows
- 1. Uninstall any existing Node.js installations first
- 2. Download nvm-windows from GitHub
- 3. Run the installer
- 4. Open PowerShell as Administrator and run:
nvm install lts
nvm use lts
node --version
Install Git for Windows
Download and install from git-scm.com. This also includes Git Bash as an alternative shell.
C
// Claude Code
Anthropic
Claude Code is Anthropic's CLI tool powered by Claude 4.5 Opus. It can read your codebase, make changes, run commands, and help you ship code faster.
Installation
npm install -g @anthropic-ai/claude-code
npm install -g @anthropic-ai/claude-code
Same command works in PowerShell, WSL2, or Git Bash.
First run
Navigate to your project folder and run:
claude
You'll be prompted to authenticate with your Anthropic account or API key.
G
// Gemini CLI
Google
Gemini CLI is Google's agentic CLI tool powered by Gemini 3.0 models. Free tier available with Google AI Studio.
npm install -g @anthropic-ai/gemini-cli
Run gemini in your project folder to start.
X
// Codex CLI
OpenAI
Codex CLI is OpenAI's coding assistant powered by GPT 5.1. Requires an OpenAI API key.
npm install -g @openai/codex
Run codex in your project folder to start.
// Status line config
A custom status line shows your model, context usage, git status, MCP servers, and active skills at a glance. Color-coded for fast scanning: the context bar shifts from green to yellow to red as you approach the limit.
Opus 4.6 | [####------] 42% / 200K | my-project (main +2 [3M 1?]) | my-session
MCPs: browser, calendar, drive, gmail | Explanatory | skills: frontend-design
01 Create the statusline script
This script receives JSON from Claude Code on stdin and outputs the formatted status line.
Save as ~/.claude/statusline-command.sh and run chmod +x
#!/bin/bash
# ANSI color codes
G='\033[32m' # green
Y='\033[33m' # yellow
C='\033[36m' # cyan
R='\033[31m' # red
M='\033[35m' # magenta
B='\033[34m' # blue
D='\033[90m' # dim gray
N='\033[0m' # reset
# Read JSON input from stdin
input=$(cat)
# Extract fields from JSON
cwd=$(echo "$input" | jq -r '.workspace.current_dir // empty')
model_name=$(echo "$input" | jq -r '.model.display_name // empty')
used_pct=$(echo "$input" | jq -r '.context_window.used_percentage // empty')
window_size=$(echo "$input" | jq -r '.context_window.context_window_size // 0')
session_name=$(echo "$input" | jq -r '.session_name // empty')
output_style=$(echo "$input" | jq -r '.output_style.name // empty')
session_id=$(echo "$input" | jq -r '.session_id // empty')
# Shorten model name
case "$model_name" in
"Claude Opus 4.6"*) short_model="Opus 4.6" ;;
"Claude Opus 4.5"*) short_model="Opus 4.5" ;;
"Claude Opus 4"*) short_model="Opus 4" ;;
"Claude Sonnet 4.6"*) short_model="Sonnet 4.6" ;;
"Claude Sonnet 4.5"*) short_model="Sonnet 4.5" ;;
"Claude Sonnet 4"*) short_model="Sonnet 4" ;;
"Claude Haiku 4.5"*) short_model="Haiku 4.5" ;;
"Claude Haiku 4"*) short_model="Haiku 4" ;;
*) short_model="$model_name" ;;
esac
# Project name: strip home prefix, then strip projects/
if [[ "$cwd" == "$HOME"* ]]; then
project_name="~${cwd#$HOME}"
else
project_name="$cwd"
fi
project_name="${project_name#'~/projects/'}"
# Git info
git_part=""
if [ -n "$cwd" ] && git -C "$cwd" rev-parse --git-dir >/dev/null 2>&1; then
branch=$(git -C "$cwd" --no-optional-locks rev-parse --abbrev-ref HEAD 2>/dev/null)
staged=$(git -C "$cwd" --no-optional-locks diff --cached --name-only 2>/dev/null | wc -l | tr -d ' ')
unstaged=$(git -C "$cwd" --no-optional-locks diff --name-only 2>/dev/null | wc -l | tr -d ' ')
untracked=$(git -C "$cwd" --no-optional-locks ls-files --others --exclude-standard 2>/dev/null | wc -l | tr -d ' ')
sync_part=""
upstream=$(git -C "$cwd" --no-optional-locks rev-parse --abbrev-ref @{upstream} 2>/dev/null)
if [ -n "$upstream" ]; then
ahead_behind=$(git -C "$cwd" --no-optional-locks rev-list --left-right --count "${upstream}...HEAD" 2>/dev/null)
behind=$(echo "$ahead_behind" | cut -f1)
ahead=$(echo "$ahead_behind" | cut -f2)
[ "${behind:-0}" -gt 0 ] && sync_part="${sync_part}-${behind}"
[ "${ahead:-0}" -gt 0 ] && sync_part="${sync_part}+${ahead}"
fi
dirty=""
[ "${staged:-0}" -gt 0 ] && dirty="${dirty}${staged}S"
[ "${unstaged:-0}" -gt 0 ] && dirty="${dirty} ${unstaged}M"
[ "${untracked:-0}" -gt 0 ] && dirty="${dirty} ${untracked}?"
dirty=$(echo "$dirty" | xargs)
git_part="$branch"
[ -n "$sync_part" ] && git_part="${git_part} ${sync_part}"
[ -n "$dirty" ] && git_part="${git_part} [${dirty}]"
fi
# Context bar with color based on usage level
context_part=""
if [ -n "$used_pct" ]; then
filled=$(awk "BEGIN {printf \"%d\", int($used_pct / 10 + 0.5)}")
[ "$filled" -gt 10 ] && filled=10
empty=$((10 - filled))
bar=""
for ((i=0; i<filled; i++)); do bar="${bar}#"; done
for ((i=0; i<empty; i++)); do bar="${bar}-"; done
if [ "$used_pct" -lt 50 ]; then
bar_color="$G"
elif [ "$used_pct" -lt 75 ]; then
bar_color="$Y"
else
bar_color="$R"
fi
ctx_k=""
if [ "${window_size:-0}" -gt 0 ]; then
ctx_k=$(awk "BEGIN {printf \"%dK\", $window_size / 1000}")
fi
if [ -n "$ctx_k" ]; then
context_part="${bar_color}[${bar}] ${used_pct}%${N} ${D}/ ${ctx_k}${N}"
else
context_part="${bar_color}[${bar}] ${used_pct}%${N}"
fi
fi
# MCP servers from settings.json
mcp_servers=$(jq -r '.mcpServers | keys | join(", ")' ~/.claude/settings.json 2>/dev/null)
# Active skills: read from hook-tracked file
active_skills=""
if [ -n "$session_id" ]; then
skills_file="/tmp/claude-skills-${session_id}"
if [ -f "$skills_file" ]; then
active_skills=$(sort -u "$skills_file" | tr '\n' ', ' | sed 's/,$//' | sed 's/,/, /g')
fi
fi
# --- Line 1: model | context | project (git) | session ---
line1="${G}${short_model}${N}"
[ -n "$context_part" ] && line1="${line1} ${D}|${N} ${context_part}"
if [ -n "$git_part" ]; then
line1="${line1} ${D}|${N} ${C}${project_name}${N} ${Y}(${git_part})${N}"
else
line1="${line1} ${D}|${N} ${C}${project_name}${N}"
fi
[ -n "$session_name" ] && line1="${line1} ${D}|${N} ${M}${session_name}${N}"
# --- Line 2: MCPs | output style | active skills ---
line2_parts=()
[ -n "$mcp_servers" ] && line2_parts+=("${D}MCPs:${N} ${R}${mcp_servers}${N}")
if [ -n "$output_style" ] && [ "$output_style" != "Normal" ] && [ "$output_style" != "null" ]; then
line2_parts+=("${B}${output_style}${N}")
fi
if [ -n "$active_skills" ]; then
line2_parts+=("${D}skills:${N} ${active_skills}")
fi
line2=""
for part in "${line2_parts[@]}"; do
[ -n "$line2" ] && line2="${line2} ${D}|${N} "
line2="${line2}${part}"
done
if [ -n "$line2" ]; then
printf "%b\n%b" "$line1" "$line2"
else
printf "%b" "$line1"
fi
Save as C:\Users\<you>\.claude\statusline-command.ps1
# statusline-command.ps1 -- Claude Code statusline for Windows
$jsonInput = [Console]::In.ReadToEnd()
$data = $jsonInput | ConvertFrom-Json
$e = [char]27
$G = "$e[32m"; $Y = "$e[33m"; $C = "$e[36m"; $R = "$e[31m"
$M = "$e[35m"; $B = "$e[34m"; $D = "$e[90m"; $N = "$e[0m"
$cwd = $data.workspace.current_dir
$modelName = $data.model.display_name
$usedPct = $data.context_window.used_percentage
$windowSize = $data.context_window.context_window_size
$sessionName = $data.session_name
$outputStyle = $data.output_style.name
$sessionId = $data.session_id
$shortModel = switch -Wildcard ($modelName) {
"Claude Opus 4.6*" { "Opus 4.6"; break }
"Claude Opus 4.5*" { "Opus 4.5"; break }
"Claude Opus 4*" { "Opus 4"; break }
"Claude Sonnet 4.6*" { "Sonnet 4.6"; break }
"Claude Sonnet 4.5*" { "Sonnet 4.5"; break }
"Claude Sonnet 4*" { "Sonnet 4"; break }
"Claude Haiku 4.5*" { "Haiku 4.5"; break }
"Claude Haiku 4*" { "Haiku 4"; break }
"Opus 4.6*" { "Opus 4.6"; break }
"Sonnet 4.6*" { "Sonnet 4.6"; break }
default { $modelName }
}
$projectName = $cwd
$home = $env:USERPROFILE
if ($projectName -and $home -and $projectName.StartsWith($home)) {
$projectName = "~" + $projectName.Substring($home.Length)
}
$projectName = $projectName -replace '^~[\\/][Pp]rojects[\\/]', ''
$gitPart = ""
try {
$null = git -C $cwd rev-parse --git-dir 2>$null
if ($LASTEXITCODE -eq 0) {
$branch = git -C $cwd --no-optional-locks rev-parse --abbrev-ref HEAD 2>$null
$staged = @(git -C $cwd --no-optional-locks diff --cached --name-only 2>$null).Count
$unstaged = @(git -C $cwd --no-optional-locks diff --name-only 2>$null).Count
$untracked = @(git -C $cwd --no-optional-locks ls-files --others --exclude-standard 2>$null).Count
$syncPart = ""
$upstream = git -C $cwd --no-optional-locks rev-parse --abbrev-ref '@{upstream}' 2>$null
if ($LASTEXITCODE -eq 0 -and $upstream) {
$ab = git -C $cwd --no-optional-locks rev-list --left-right --count "$upstream...HEAD" 2>$null
if ($ab) {
$parts = $ab -split '\t'
if ([int]$parts[0] -gt 0) { $syncPart += "-$($parts[0])" }
if ([int]$parts[1] -gt 0) { $syncPart += "+$($parts[1])" }
}
}
$dirty = ""
if ($staged -gt 0) { $dirty += "${staged}S" }
if ($unstaged -gt 0) { if ($dirty) { $dirty += " " }; $dirty += "${unstaged}M" }
if ($untracked -gt 0) { if ($dirty) { $dirty += " " }; $dirty += "${untracked}?" }
$gitPart = $branch
if ($syncPart) { $gitPart += " $syncPart" }
if ($dirty) { $gitPart += " [$dirty]" }
}
} catch {}
$contextPart = ""
if ($null -ne $usedPct) {
$filled = [Math]::Round($usedPct / 10)
if ($filled -gt 10) { $filled = 10 }
$bar = ('#' * $filled) + ('-' * (10 - $filled))
if ($usedPct -lt 50) { $barColor = $G }
elseif ($usedPct -lt 75) { $barColor = $Y }
else { $barColor = $R }
$ctxK = if ($windowSize -gt 0) { [Math]::Floor($windowSize / 1000).ToString() + "K" } else { "" }
if ($ctxK) { $contextPart = "${barColor}[${bar}] ${usedPct}%${N} ${D}/ ${ctxK}${N}" }
else { $contextPart = "${barColor}[${bar}] ${usedPct}%${N}" }
}
$mcpServers = ""
$sp = Join-Path $env:USERPROFILE ".claude\settings.json"
if (Test-Path $sp) {
try {
$s = Get-Content $sp -Raw | ConvertFrom-Json
if ($s.mcpServers) { $mcpServers = ($s.mcpServers.PSObject.Properties.Name | Sort-Object) -join ", " }
} catch {}
}
$activeSkills = ""
if ($sessionId) {
$sf = Join-Path $env:TEMP "claude-skills-$sessionId"
if (Test-Path $sf) { $activeSkills = (Get-Content $sf | Sort-Object -Unique) -join ", " }
}
$line1 = "${G}${shortModel}${N}"
if ($contextPart) { $line1 += " ${D}|${N} $contextPart" }
if ($gitPart) { $line1 += " ${D}|${N} ${C}${projectName}${N} ${Y}(${gitPart})${N}" }
else { $line1 += " ${D}|${N} ${C}${projectName}${N}" }
if ($sessionName) { $line1 += " ${D}|${N} ${M}${sessionName}${N}" }
$line2Parts = @()
if ($mcpServers) { $line2Parts += "${D}MCPs:${N} ${R}$mcpServers${N}" }
if ($outputStyle -and $outputStyle -ne "Normal") { $line2Parts += "${B}$outputStyle${N}" }
if ($activeSkills) { $line2Parts += "${D}skills:${N} $activeSkills" }
$line2 = $line2Parts -join " ${D}|${N} "
if ($line2) { [Console]::Out.Write("$line1`n$line2") }
else { [Console]::Out.Write($line1) }
02 Add the skill tracking hook
This hook runs after each skill invocation and records the skill name so the statusline can display it.
Save as ~/.claude/hooks/track-active-skill.sh and chmod +x
#!/bin/bash
input=$(cat)
skill=$(echo "$input" | jq -r '.tool_input.skill // empty')
session_id=$(echo "$input" | jq -r '.session_id // empty')
if [ -n "$skill" ] && [ -n "$session_id" ]; then
file="/tmp/claude-skills-${session_id}"
grep -qxF "$skill" "$file" 2>/dev/null || echo "$skill" >> "$file"
fi
exit 0
Save as C:\Users\<you>\.claude\hooks\track-active-skill.ps1
$jsonInput = [Console]::In.ReadToEnd()
$data = $jsonInput | ConvertFrom-Json
$skill = $data.tool_input.skill
$sessionId = $data.session_id
if ($skill -and $sessionId) {
$file = Join-Path $env:TEMP "claude-skills-$sessionId"
$existing = @()
if (Test-Path $file) { $existing = @(Get-Content $file) }
if ($skill -notin $existing) { Add-Content -Path $file -Value $skill }
}
exit 0
03 Wire it up in settings.json
Add these entries to your ~/.claude/settings.json. If you already have a PostToolUse hooks array, add the Skill matcher before existing entries.
{
"statusLine": {
"type": "command",
"command": "/home/YOU/.claude/statusline-command.sh"
},
"hooks": {
"PostToolUse": [
{
"matcher": "Skill",
"hooks": [{
"type": "command",
"command": "/home/YOU/.claude/hooks/track-active-skill.sh"
}]
}
]
}
}
{
"statusLine": {
"type": "command",
"command": "powershell -NoProfile -ExecutionPolicy Bypass -File C:\\Users\\YOU\\.claude\\statusline-command.ps1"
},
"hooks": {
"PostToolUse": [
{
"matcher": "Skill",
"hooks": [{
"type": "command",
"command": "powershell -NoProfile -ExecutionPolicy Bypass -File C:\\Users\\YOU\\.claude\\hooks\\track-active-skill.ps1"
}]
}
]
}
}
Replace YOU with your username. The bash script requires jq — install with brew install jq (Mac) or sudo apt install jq (Linux). Windows uses built-in PowerShell JSON parsing.
04 Reading the status line
Line 1
Model name, context window bar (color shifts as usage grows), project directory with git branch and dirty file counts (S=staged, M=modified, ?=untracked), and session name.
Line 2
Connected MCP servers, active output style (hidden when set to Normal), and skills invoked during the current session.
Git sync
+3 = 3 commits ahead of remote, -1 = 1 behind. Shown next to the branch name when applicable.
// Troubleshooting
"command not found" after installation
Your terminal can't find the installed package. Try restarting your terminal, or check that npm's bin directory is in your PATH.
npm config get prefix # Shows where npm installs global packages
Permission errors on Mac/Linux
Don't use sudo npm install -g. Instead, fix npm permissions or use nvm.
API key issues
Make sure your API key is valid and has the correct permissions. Each tool stores keys differently - check their documentation for details.