A Git-Aware PowerShell Prompt That Earns Its Keep


I spend most of my working day in terminals. On Windows, that means PowerShell Core, where the default prompt is fairly minimal: a path and a greater-than sign, with no information about which branch you’re on, whether you have uncommitted work, or whether there’s a stash you set aside a while ago and haven’t returned to.

So I put together a small replacement.

What It Does

The prompt is built from three segments, each colour-coded to deliver information at a glance:

The server tag — a grey block showing the logon server name (title-cased) followed by a 𝚸 glyph. It’s a small orientation aid when you’re hopping between machines via RDP or remote sessions. You always know where you are.

$LogonServer = ConvertTo-TitleCase -InputString $env:LOGONSERVER
Write-Host -NoNewline "${LogonServer}[`u{1D6B8}] " -BackgroundColor Gray -ForegroundColor Black

The working directory — blue background for normal sessions, dark red when running as Administrator. The colour distinction has helped me catch a few accidental elevated operations. Remove-Item -Recurse doesn’t ask for confirmation, so a permanent visual cue is useful.

$isAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole(
  [Security.Principal.WindowsBuiltInRole]::Administrator)

$location = (Get-Location).Path
if ($isAdmin) {
  Write-Host -NoNewline "$($location) " -ForegroundColor Gray -BackgroundColor DarkRed
} else {
  Write-Host -NoNewline "$($location) " -BackgroundColor Blue -ForegroundColor Black
}

The git segment — this is where it gets interesting. When you’re inside a repository, a fourth block appears with:

Outside a git repository, the segment simply disappears. No noise.

The colour switching and indicator assignment look like this:

$isDirty = $hasUnstagedChanges -or $hasStagedChanges -or $hasUntrackedFiles
$bgColor = if ($isDirty) { "DarkYellow" } else { "DarkGreen" }

$stashCount = (git stash list 2>$null).Count
$stashInfo = if ($stashCount -gt 0) { " `u{1D6B8}:$stashCount " } else { "" }

$unstagedIndicator   = if ($hasUnstagedChanges) { "`u{1D6B3} " } else { "" }
$stagedIndicator     = if ($hasStagedChanges)   { "`u{1D6B1} " } else { "" }
$untrackedIndicator  = if ($hasUntrackedFiles)  { "`u{1D6B0}"  } else { "" }

Write-Host -NoNewline "`u{1D6A9}:" -ForegroundColor Black -BackgroundColor $bgColor
Write-Host -NoNewline "$gitBranch" -ForegroundColor Black -BackgroundColor $bgColor
# ... stash count and status indicators follow
Write-Host -NoNewline "$untrackedIndicator$unstagedIndicator$stagedIndicator" `
  -ForegroundColor Black -BackgroundColor $bgColor

Why Not Oh My Posh or Starship?

Both are excellent tools, and for many setups they’re the right choice. They’re also extra dependencies — binaries to install, configure with TOML or JSON, and keep updated. For something as fundamental as my prompt, I prefer a single function in my $PROFILE that I can read end to end and modify in a few seconds. No theme files, no font requirements beyond a Unicode-capable terminal, and one less moving part to debug after an update.

The entire thing is plain PowerShell. It calls git branch --show-current and git status --porcelain directly, parses the two-character status codes, and sets colours accordingly. The --porcelain flag matters here — it gives machine-readable output that’s stable across git versions, unlike the human-readable git status which is localised and can change format.

The Parsing

The git status --porcelain format uses a two-character prefix per file:

The code walks each line and sets three boolean flags. It doesn’t count files or build lists — it only needs to know whether each category has entries, not how many. That keeps it fast even in a repository with thousands of changed files, where you’d rather the prompt didn’t do more work than necessary.

$gitStatusOutput = git status --porcelain 2>$null

$hasUnstagedChanges = $false
$hasStagedChanges   = $false
$hasUntrackedFiles  = $false

if ($gitStatusOutput) {
  foreach ($line in $gitStatusOutput) {
    if ($line.StartsWith('??')) {
      $hasUntrackedFiles = $true
      continue
    }
    # Second character: work tree (unstaged) status
    if ($line.Length -gt 1 -and $line[1] -ne ' ') {
      $hasUnstagedChanges = $true
    }
    # First character: index (staged) status
    if ($line.Length -gt 0 -and $line[0] -ne ' ' -and $line[0] -ne '?') {
      $hasStagedChanges = $true
    }
  }
}

The ConvertTo-TitleCase Helper

A small utility at the top normalises the LOGONSERVER environment variable, which Windows stores in upper case with leading backslashes. It finds the first letter, uppercases it, and lowercases the rest — just enough to make the prompt easier to scan.

function ConvertTo-TitleCase {
  param(
    [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
    [string]$InputString
  )

  if ([string]::IsNullOrEmpty($InputString)) { return $InputString }

  for ($i = 0; $i -lt $InputString.Length; $i++) {
    if ([char]::IsLetter($InputString[$i])) {
      return $InputString.Substring(0, $i) +
             $InputString[$i].ToString().ToUpper() +
             $InputString.Substring($i + 1).ToLower()
    }
  }

  return $InputString
}

Living With It

I’ve used variations of this prompt for a while, and the part I appreciate most is the colour coding. A glance at the terminal tells me:

None of that requires reading — it’s all peripheral.

If you’d like to try it, drop the functions into your PowerShell profile (code $PROFILE) and make sure your terminal supports Unicode. Windows Terminal does out of the box. The Mathematical Bold Capital glyphs (U+1D6A9 and friends) render in most modern terminals and give the prompt a distinctive look without requiring a Nerd Font.

Here’s the full source — drop it into your $PROFILE and you’re done:

function ConvertTo-TitleCase {
  param(
    [Parameter(Mandatory = $true, ValueFromPipeline = $true)]
    [string]$InputString
  )

  if ([string]::IsNullOrEmpty($InputString)) {
    return $InputString
  }

  # Find the first letter and capitalize it
  for ($i = 0; $i -lt $InputString.Length; $i++) {
    if ([char]::IsLetter($InputString[$i])) {
      return $InputString.Substring(0, $i) + $InputString[$i].ToString().ToUpper() + $InputString.Substring($i + 1).ToLower()
    }
  }

  # If no letter found, return original string
  return $InputString
}

# Enhanced git prompt for PowerShell Core
function prompt {

  # Show logon server with a PowerShell icon and convert to title case
  $LogonServer = ConvertTo-TitleCase -InputString $env:LOGONSERVER
  Write-Host -NoNewline "${LogonServer}[`u{1D6B8}] " -BackgroundColor Gray -ForegroundColor Black

  # Check if running as admin
  $isAdmin = ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)

  # Start with the location part of the prompt
  $location = (Get-Location).Path
  if ($isAdmin) {
    Write-Host -NoNewline "$($location) " -ForegroundColor Gray -BackgroundColor DarkRed
  }
  else {
    Write-Host -NoNewline "$($location) " -BackgroundColor Blue -ForegroundColor Black
  }

  # Get Git branch and status if in a git repository
  if (Get-Command git -ErrorAction SilentlyContinue) {
    try {
      $gitBranch = git branch --show-current 2>$null
      if ($gitBranch) {
        # Get all status output
        $gitStatusOutput = git status --porcelain 2>$null

        # Initialize status flags
        $hasUnstagedChanges = $false
        $hasStagedChanges = $false
        $hasUntrackedFiles = $false

        if ($gitStatusOutput) {
          foreach ($line in $gitStatusOutput) {
            # Check for untracked files (indicated by '??')
            if ($line.StartsWith('??')) {
              $hasUntrackedFiles = $true
              continue
            }

            # Check if the second character is not a space (indicating unstaged change)
            if ($line.Length -gt 1 -and $line[1] -ne ' ') {
              $hasUnstagedChanges = $true
            }

            # Check if the first character is not a space (indicating staged change)
            if ($line.Length -gt 0 -and $line[0] -ne ' ' -and $line[0] -ne '?') {
              $hasStagedChanges = $true
            }
          }
        }

        # Set background color based on whether there are any changes
        $isDirty = $hasUnstagedChanges -or $hasStagedChanges -or $hasUntrackedFiles
        $bgColor = if ($isDirty) {
          "DarkYellow"
        }
        else {
          "DarkGreen"
        }

        # Check for stashes
        $stashCount = (git stash list 2>$null).Count
        $stashInfo = if ($stashCount -gt 0) {
          " `u{1D6B8}:$stashCount "
        }
        else {
          ""
        }

        $unstagedIndicator = if ($hasUnstagedChanges) {
          "`u{1D6B3} "
        }
        else {
          ""
        }
        $stagedIndicator = if ($hasStagedChanges) {
          "`u{1D6B1} "
        }
        else {
          ""
        }
        $untrackedIndicator = if ($hasUntrackedFiles) {
          "`u{1D6B0}"
        }
        else {
          ""
        }

        # Write opening angle bracket with background color
        Write-Host -NoNewline "`u{1D6A9}:" -ForegroundColor Black -BackgroundColor $bgColor

        # Write branch name
        Write-Host -NoNewline "$gitBranch" -ForegroundColor Black -BackgroundColor $bgColor

        # Add stash count if there are stashes
        if ($stashCount -gt 0) {
          Write-Host -NoNewline $stashInfo -ForegroundColor Black -BackgroundColor $bgColor
        }
        elseif ($isDirty) {
          # Write a space if there are no stashes
          Write-Host -NoNewline " " -ForegroundColor Black -BackgroundColor $bgColor
        }

        # Add status indicators
        Write-Host -NoNewline "$untrackedIndicator$unstagedIndicator$stagedIndicator" -ForegroundColor Black -BackgroundColor $bgColor

        # Determine if we need a separator
        $hasAnyIndicators = $stashCount -gt 0 -or $hasUnstagedChanges -or $hasStagedChanges -or $hasUntrackedFiles
        if ($hasAnyIndicators -eq $false) {
          Write-Host -NoNewline " " -ForegroundColor Black -BackgroundColor $bgColor
        }
      }
    }
    catch {
      # Not in a git repository or git command failed
    }
  }

  # Close the bracket
  if ($isAdmin) {
    Write-Host -NoNewline ">"
  }
  else {
    Write-Host -NoNewline ">"
  }

  # Return a space for the actual prompt
  return " "
}