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 versionNew-Item -ItemType Directory -Force "$env:USERPROFILE\.copilot" | Out-NullCreate 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-JsonThen 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=falseThe --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_LINEis enabled.statusLine.commandpoints to the.cmdwrapper.- 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=falseAvoid 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.