cd /news/ai-tools/use-oh-my-posh-for-the-github-copilo… · home topics ai-tools article
[ARTICLE · art-18266] src=gist.github.com pub= topic=ai-tools verified=true sentiment=· neutral

Use Oh My Posh for the GitHub Copilot CLI statusline

GitHub Copilot CLI's experimental statusline feature can now be rendered using Oh My Posh, allowing developers to display session state—including git branch, token usage, duration, and line changes—directly in the Copilot terminal UI. A Windows and PowerShell setup uses a local script that receives Copilot session data as JSON via stdin, maps it to environment variables, and feeds it into a custom Oh My Posh theme. The configuration requires three files in `%USERPROFILE%\.copilot\`—a command wrapper, a PowerShell parser, and a JSON theme—along with updates to Copilot's settings and feature flags.

read8 min publishedMay 5, 2026

GitHub Copilot CLI has an experimental statusline feature that can run a local command and render the command's output at the bottom of the Copilot terminal UI.

If you already use Oh My Posh, you can use the same engine, colors, and segments to render a Copilot-aware statusline in a few minutes.

This guide shows a Windows + PowerShell setup, but the idea is portable:

  • Copilot calls a local script.
  • Copilot sends session state to the script as JSON on stdin.
  • The script maps useful values into environment variables.
  • Oh My Posh renders a small statusline theme using those variables.

If you want a coding agent to set this up for you, point it at this gist and give it this prompt:

Set up GitHub Copilot CLI's experimental statusline using Oh My Posh by following this gist.

Use my existing Oh My Posh theme as inspiration, but create a small statusline-specific theme instead of blindly reusing my full shell prompt. Keep it fast. Do not include secrets, tokens, private URLs, personal data, customer data, or anything that should not appear in screenshots.

Acceptance criteria:
1. Create %USERPROFILE%\.copilot\statusline.cmd.
2. Create %USERPROFILE%\.copilot\statusline.ps1.
3. Create %USERPROFILE%\.copilot\statusline.omp.json.
4. Update %USERPROFILE%\.copilot\settings.json with statusLine.command pointing at statusline.cmd.
5. Enable the STATUS_LINE feature flag.
6. Test the command by piping a sample Copilot payload into statusline.cmd.
7. Tell me to run /restart in Copilot CLI if it is already open.

The agent should preserve any existing Copilot settings, merge into feature_flags.enabled

instead of replacing the array, and back up settings.json

before editing it.

Example shape:

<git branch> <runtime/tooling> <context tokens> <context gauge> <duration> <line changes>

For example:

main +2/-1 | .NET 10.0 | 123.5k/200.0k | ######.... | 00:12:34 | +42/-8

The exact appearance depends on your Oh My Posh theme and Nerd Font.

  • GitHub Copilot CLI with the experimental statusline feature.

  • PowerShell 7: pwsh

  • Oh My Posh installed and available on PATH

. - A terminal font that supports Nerd Font glyphs if your theme uses icons.

Check Oh My Posh:

oh-my-posh version
New-Item -ItemType Directory -Force "$env:USERPROFILE\.copilot" | Out-Null

Create this file:

%USERPROFILE%\.copilot\statusline.cmd

Contents:

@echo off
pwsh -NoProfile -ExecutionPolicy Bypass -File "%~dp0statusline.ps1"

Why the wrapper? In testing, Copilot's statusLine.command

setting was most reliable when it pointed at a command/script path. Putting pwsh -File ...

directly in the JSON setting can be less reliable on Windows. The wrapper also preserves stdin, which is how Copilot sends the payload.

Create this file:

%USERPROFILE%\.copilot\statusline.ps1

Contents:

$ErrorActionPreference = 'Stop'
[Console]::OutputEncoding = [System.Text.UTF8Encoding]::new()

function Format-TokenCount {
    param([Nullable[double]]$Value)

    if ($null -eq $Value) { return '?' }
    if ($Value -ge 1000000) { return ('{0:0.0}m' -f ($Value / 1000000)) }
    if ($Value -ge 1000) { return ('{0:0.0}k' -f ($Value / 1000)) }
    return ([int]$Value).ToString()
}

function Format-Duration {
    param([Nullable[double]]$Milliseconds)

    if ($null -eq $Milliseconds -or $Milliseconds -le 0) { return '00:00:00' }
    $duration = [TimeSpan]::FromMilliseconds($Milliseconds)
    return '{0:00}:{1:00}:{2:00}' -f [int]$duration.TotalHours, $duration.Minutes, $duration.Seconds
}

function New-Gauge {
    param([Nullable[double]]$Percent)

    if ($null -eq $Percent) { return '..........' }
    $bounded = [Math]::Max(0, [Math]::Min(100, [Math]::Round($Percent)))
    $filled = [int][Math]::Floor($bounded / 10)
    return ('#' * $filled) + ('.' * (10 - $filled))
}

$payload = [Console]::In.ReadToEnd()

try {
    $json = $payload | ConvertFrom-Json
} catch {
    Write-Host -NoNewline 'Copilot status unavailable'
    exit 0
}

$context = $json.context_window
$cost = $json.cost

$currentTokens = if ($null -ne $context.current_context_tokens) {
    [double]$context.current_context_tokens
} else {
    $null
}

$contextLimit = if ($null -ne $context.displayed_context_limit) {
    [double]$context.displayed_context_limit
} else {
    $null
}

$contextPercent = if ($null -ne $context.current_context_used_percentage) {
    [double]$context.current_context_used_percentage
} elseif ($null -ne $context.used_percentage) {
    [double]$context.used_percentage
} else {
    $null
}

$linesAdded = if ($null -ne $cost.total_lines_added) { [int]$cost.total_lines_added } else { 0 }
$linesRemoved = if ($null -ne $cost.total_lines_removed) { [int]$cost.total_lines_removed } else { 0 }

$env:COPILOT_STATUS_CONTEXT = "$(Format-TokenCount $currentTokens)/$(Format-TokenCount $contextLimit)"
$env:COPILOT_STATUS_GAUGE = New-Gauge $contextPercent
$env:COPILOT_STATUS_DURATION = Format-Duration $cost.total_duration_ms
$env:COPILOT_STATUS_CHANGES = if ($linesAdded -or $linesRemoved) { "+$linesAdded/-$linesRemoved" } else { '' }

$theme = Join-Path $PSScriptRoot 'statusline.omp.json'
$cwd = if ($json.cwd) { [string]$json.cwd } else { (Get-Location).Path }

try {
    $output = & oh-my-posh print primary --config $theme --pwd $cwd --force --escape=false 2>$null
    if ([string]::IsNullOrWhiteSpace($output)) {
        throw 'Oh My Posh returned no output.'
    }

    Write-Host -NoNewline $output.TrimEnd()
} catch {
    $changes = if ($env:COPILOT_STATUS_CHANGES) { " $($env:COPILOT_STATUS_CHANGES)" } else { '' }
    Write-Host -NoNewline "ctx $($env:COPILOT_STATUS_CONTEXT) $($env:COPILOT_STATUS_GAUGE) time $($env:COPILOT_STATUS_DURATION)$changes"
}

Create this file:

%USERPROFILE%\.copilot\statusline.omp.json

Contents:

{
  "$schema": "https://raw.githubusercontent.com/JanDeDobbeleer/oh-my-posh/main/themes/schema.json",
  "version": 3,
  "final_space": false,
  "blocks": [
    {
      "type": "prompt",
      "alignment": "left",
      "segments": [
        {
          "type": "git",
          "style": "diamond",
          "leading_diamond": "<",
          "trailing_diamond": ">",
          "foreground": "#193549",
          "background": "#FFA500",
          "template": " {{ .HEAD }}{{ if .Working.Changed }} +{{ .Working.String }}{{ end }} ",
          "properties": {
            "fetch_status": true,
            "fetch_upstream_icon": true
          }
        },
        {
          "type": "dotnet",
          "style": "powerline",
          "powerline_symbol": ">",
          "foreground": "#ffffff",
          "background": "#6CA35E",
          "template": " .NET {{ if .Unsupported }}!{{ else }}{{ .Full }}{{ end }} ",
          "properties": {
            "fetch_version": true
          }
        },
        {
          "type": "text",
          "style": "powerline",
          "powerline_symbol": ">",
          "foreground": "#193549",
          "background": "#FFA500",
          "template": " ctx {{ .Env.COPILOT_STATUS_CONTEXT }} "
        },
        {
          "type": "text",
          "style": "powerline",
          "powerline_symbol": ">",
          "foreground": "#ffffff",
          "background": "#6CA35E",
          "template": " {{ .Env.COPILOT_STATUS_GAUGE }} "
        },
        {
          "type": "text",
          "style": "powerline",
          "powerline_symbol": ">",
          "foreground": "#ffffff",
          "background": "#0184bc",
          "template": " {{ .Env.COPILOT_STATUS_DURATION }} "
        },
        {
          "type": "text",
          "style": "diamond",
          "trailing_diamond": ">",
          "foreground": "#ffffff",
          "background": "#a1108c",
          "template": "{{ if .Env.COPILOT_STATUS_CHANGES }} {{ .Env.COPILOT_STATUS_CHANGES }} {{ end }}"
        }
      ]
    }
  ]
}

This intentionally uses plain ASCII separators so it works everywhere. If you already use a Nerd Font, replace the diamonds and separators with your favorite powerline glyphs.

For example:

"leading_diamond": "\ue0b6",
"trailing_diamond": "\ue0b0",
"powerline_symbol": "\ue0b0"

Edit:

%USERPROFILE%\.copilot\settings.json

Add or merge this:

{
  "statusLine": {
    "type": "command",
    "command": "C:\\Users\\YOURUSER\\.copilot\\statusline.cmd",
    "padding": 1
  },
  "feature_flags": {
    "enabled": [
      "STATUS_LINE"
    ]
  },
  "experimental": true
}

Replace YOURUSER

with your Windows username.

If you already have feature_flags.enabled

, add STATUS_LINE

to the existing array instead of replacing it.

Restart Copilot CLI:

/restart

Create a sample payload:

$sample = @'
{
  "cwd": "C:\\src\\my-repo",
  "context_window": {
    "current_context_tokens": 123456,
    "displayed_context_limit": 200000,
    "current_context_used_percentage": 61.7
  },
  "cost": {
    "total_duration_ms": 754000,
    "total_lines_added": 42,
    "total_lines_removed": 8
  }
}
'@

$sample | & "$env:USERPROFILE\.copilot\statusline.cmd"

If that renders, Copilot should be able to render it too.

Copilot sends JSON to your command over stdin. The script reads that JSON:

$payload = [Console]::In.ReadToEnd()
$json = $payload | ConvertFrom-Json

Then it turns useful fields into environment variables:

$env:COPILOT_STATUS_CONTEXT = "123.5k/200.0k"
$env:COPILOT_STATUS_GAUGE = "######...."
$env:COPILOT_STATUS_DURATION = "00:12:34"
$env:COPILOT_STATUS_CHANGES = "+42/-8"

Oh My Posh templates can read those values:

"template": " ctx {{ .Env.COPILOT_STATUS_CONTEXT }} "

Finally, the script asks Oh My Posh to render the mini theme:

oh-my-posh print primary --config $theme --pwd $cwd --force --escape=false

The --pwd

value matters. It lets normal Oh My Posh segments, such as git

, render for the repository Copilot is working in.

Do not point the statusline directly at your full interactive shell theme first. A full prompt theme can be too wide or too slow for a statusline.

Instead:

  • Copy two or three favorite segments from your existing theme.
  • Keep any expensive or network-backed segments out at first.
  • Add Copilot-specific text segments using .Env.COPILOT_STATUS_*

. - Test with the sample payload.

  • Add more segments only if the command stays fast.

Good statusline segments:

git

  • language/runtime version segments, such as dotnet

,node

,python

, orgo

  • simple text segments using Copilot environment variables
  • short path or folder segments

Segments to be careful with:

  • anything that makes network calls
  • anything that scans large directories
  • anything that can prompt for credentials

The statusline command needs to finish quickly. If it times out, Copilot may show no statusline at all.

Check:

STATUS_LINE

is enabled.statusLine.command

points to the.cmd

wrapper.- You restarted Copilot CLI after changing settings.

  • The command works with the sample payload.

Use the wrapper path in settings.json

:

"command": "C:\\Users\\YOURUSER\\.copilot\\statusline.cmd"

Avoid putting a full command with arguments directly in the setting.

For this use case, call:

oh-my-posh print primary --config $theme --pwd $cwd --force --escape=false

Avoid adding --shell pwsh

unless you have verified it still uses the intended statusline theme.

Use a Nerd Font in your terminal profile, or keep the theme ASCII-only.

Remove slow segments and retest. Start with only text segments. Then add git

. Then add runtime segments. Add network-backed segments last, with strict timeouts.

Treat your statusline like anything else printed in a terminal: it can appear in screenshots, recordings, livestreams, and logs. Do not render secrets, tokens, private URLs, customer names, personal data, or other sensitive values.

If a segment needs a private URL or token, keep that value in a local config file or environment variable and do not commit it to a repo or gist.

The statusline feature is just a small command contract:

Copilot JSON on stdin -> your script -> one line of text on stdout

Oh My Posh is a great renderer for that one line because it already knows how to draw prompt segments, colors, icons, and repository-aware context.

── more in #ai-tools 4 stories · sorted by recency
sponsored brought to you by zahid.host 4,200+ EU-deployed projects
reading about agents? ship yours in a single git push.

Run your AI side-project on zahid.host

EU-based hosting, git-push deploys, automatic HTTPS, no cold starts. Free tier with a custom domain — perfect for shipping the agent you just read about.

$git push zahid main
Live at https://your-agent.zahid.host
Get free account → Pricing
from €0/mo · no card required
LIVE [news/use-oh-my-posh-for-t…] indexed:0 read:8min 2026-05-05 ·