Skip to content

Instantly share code, notes, and snippets.

@shanselman
Last active May 15, 2026 00:54
Show Gist options
  • Select an option

  • Save shanselman/9623ac74888a07ba82f63f5310fda11b to your computer and use it in GitHub Desktop.

Select an option

Save shanselman/9623ac74888a07ba82f63f5310fda11b to your computer and use it in GitHub Desktop.
Use Oh My Posh for the GitHub Copilot CLI statusline

Use Oh My Posh for the GitHub Copilot CLI statusline

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:

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

Agent quickstart

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.

What you get

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.

Requirements

  • 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

1. Create the statusline folder

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

2. Create the Windows command wrapper

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.

3. Create the PowerShell renderer

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"
}

4. Create a small Oh My Posh statusline theme

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"

5. Enable Copilot's statusline command

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

6. Test it without Copilot

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.

How it works

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.

Adapting your existing Oh My Posh theme

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:

  1. Copy two or three favorite segments from your existing theme.
  2. Keep any expensive or network-backed segments out at first.
  3. Add Copilot-specific text segments using .Env.COPILOT_STATUS_*.
  4. Test with the sample payload.
  5. Add more segments only if the command stays fast.

Good statusline segments:

  • git
  • language/runtime version segments, such as dotnet, node, python, or go
  • 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.

Troubleshooting

The line does not show up

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.

It works manually but not in Copilot

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.

Oh My Posh renders the wrong thing

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.

Icons look broken

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

The line disappears sometimes

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.

Privacy note

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 key lesson

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment