A
Terminal Setup
AI CLI Tools v1.0
System: Terminal Init

Terminal Setup Guide

Configure your terminal for Claude Code, Gemini CLI, and Codex. Includes status line configuration and platform-specific setup.

status_line_demo.sh
user/my-project | main | S: 0 | U: 2 | A: 1 | 23.4%

// 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.

Claude Code: 18+ Gemini CLI: 20+ Codex: 18+

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:

Warp (recommended for beginners)
FREE

Modern terminal with built-in AI assistance. Great for learning commands.

warp.dev
iTerm2 (power users)
FREE

Highly customizable, split panes, search, and extensive features.

iterm2.com
Terminal.app (built-in)
DEFAULT

Works fine for basic use. No installation needed.

Homebrew is the easiest way to install developer tools on Mac. Open Terminal and run:

Install Homebrew
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
Apple Silicon Macs (M1/M2/M3/M4)

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

Terminal
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 + Windows Terminal (recommended)
BEST

WSL2 gives you a full Linux environment inside Windows. Most programming tutorials assume Unix-like systems, so this makes following them much easier.

PowerShell
NATIVE

Modern Windows shell. Works for all three CLI tools but some commands differ from tutorials.

Command Prompt (cmd)
LEGACY

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

PowerShell (Admin)
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. 1. Uninstall any existing Node.js installations first
  2. 2. Download nvm-windows from GitHub
  3. 3. Run the installer
  4. 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

Terminal
npm install -g @anthropic-ai/claude-code
PowerShell / WSL2
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.

Installation
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.

Installation
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.

Preview
Opus 4.6 | [####------] 42% / 200K | my-project (main +2 [3M 1?]) | my-session
MCPs: browser, calendar, drive, gmail | Explanatory | skills: frontend-design
Model
Project
Git branch
Session
MCPs
Output style
Labels / pipes
Context bar

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

statusline-command.sh
#!/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
# 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

track-active-skill.sh
#!/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

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.

~/.claude/settings.json
{
  "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"
        }]
      }
    ]
  }
}
C:\Users\YOU\.claude\settings.json
{
  "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.