What can trigger a notification?
These are the four events currently wired into the Windows notification layer.
Permission
Copilot needs approval to execute a tool.
Subagent complete
A background Copilot agent or subagent has finished.
Input required
Copilot is waiting for additional information.
Main agent finished
The main Copilot agent has finished a turn.
1. Purpose
This document records the Windows notification system built around GitHub Copilot CLI.
The goal was not simply to make Copilot produce notifications. Copilot CLI already exposes notification hooks. The goal was to build a Windows desktop notification layer that is context-aware:
- Notify when Copilot needs permission.
- Notify when Copilot needs user input.
- Notify when a background Copilot agent/subagent finishes.
- Notify when the main Copilot agent finishes a turn.
- Do not show a Windows toast when the user is already looking at that exact Copilot CLI tab.
- Show the toast when Copilot is running in another Windows Terminal tab.
- Show the toast when the user has moved to another application such as Brave/Chrome/VS Code.
The resulting system uses native Copilot CLI hooks for event detection and custom PowerShell + Windows APIs + UI Automation + BurntToast for the Windows-side behavior.
2. What is native Copilot CLI functionality vs. what we built
Native Copilot CLI
Copilot CLI provides a notification hook. The current hook reference documents notification types including:
shell_completedshell_detached_completedagent_completedagent_idlepermission_promptelicitation_dialog
The CLI also exposes agentStop, which fires when the main agent finishes a turn.
The notification hook is asynchronous/fire-and-forget and supports a matcher against notification_type.
Custom functionality built here
Copilot CLI does not provide the exact desktop behavior we wanted:
Only show a Windows toast when the Copilot session is not currently visible to the user.
The custom layer therefore handles:
- Windows toast rendering through BurntToast.
- Foreground-window detection through Win32.
- Windows Terminal tab discovery through UI Automation.
- Detection of the exact selected Terminal tab.
- A stable Copilot tab identity based on
WT_SESSION. - Suppression of the toast when that Copilot tab is selected.
Conceptually:
Copilot CLI
|
| native hook event
v
notification-hooks.json
|
v
copilot-notify.ps1
|
+---------+---------+
| |
v v
Foreground check Tab-selection check
| |
+---------+---------+
|
+---------+---------+
| |
Copilot tab visible Copilot in background
| |
no toast toast
3. Final architecture
GitHub Copilot CLI
|
| notification / agentStop
v
~/.copilot/hooks/notification-hooks.json
|
| invokes PowerShell
v
~/.copilot/hooks/copilot-notify.ps1
|
+--> Is foreground app Windows Terminal?
|
+--> If yes, inspect Windows Terminal UI Automation
|
+--> Find "Copilot [SESSION]"
|
+--> Is that tab selected?
| |
| +--> Yes: EXIT, no toast
| |
| +--> No: show toast
|
+--> If foreground app is not Windows Terminal:
show toast
|
v
BurntToast
|
v
Windows notification
4. Files created
The system uses these files:
%USERPROFILE%\.copilot\
└── hooks\
├── notification-hooks.json
└── copilot-notify.ps1
A PowerShell profile wrapper was also added so copilot can create a stable terminal-tab title:
$PROFILE
The BurntToast module is installed as a PowerShell module.
5. Notification events currently configured
The current configuration sends notifications for four events.
| Event | Meaning | Current toast |
|---|---|---|
permission_prompt |
Copilot needs approval to execute something | Copilot needs approval |
agent_completed |
A background/subagent finishes | Copilot subagent finished |
elicitation_dialog |
Copilot needs additional user information | Copilot needs your input |
agentStop |
Main Copilot agent finishes a turn | Copilot finished |
The current notification text is:
permission_prompt
Title: Copilot needs approval
Message: Copilot is waiting for your permission.
agent_completed
Title: Copilot subagent finished
Message: A background Copilot agent has completed its work.
elicitation_dialog
Title: Copilot needs your input
Message: Copilot is waiting for additional information.
agentStop
Title: Copilot finished
Message: Copilot has finished its task.
The following notification types are not currently configured:
shell_completed
shell_detached_completed
agent_idle
They can be added later without changing the foreground/background mechanism.
6. notification-hooks.json
The current configuration is:
{
"version": 1,
"hooks": {
"notification": [
{
"type": "command",
"matcher": "permission_prompt",
"powershell": "& \"$env:USERPROFILE\\.copilot\\hooks\\copilot-notify.ps1\" -Title 'Copilot needs approval' -Message 'Copilot is waiting for your permission.'",
"timeoutSec": 5
},
{
"type": "command",
"matcher": "agent_completed",
"powershell": "& \"$env:USERPROFILE\\.copilot\\hooks\\copilot-notify.ps1\" -Title 'Copilot subagent finished' -Message 'A background Copilot agent has completed its work.'",
"timeoutSec": 5
},
{
"type": "command",
"matcher": "elicitation_dialog",
"powershell": "& \"$env:USERPROFILE\\.copilot\\hooks\\copilot-notify.ps1\" -Title 'Copilot needs your input' -Message 'Copilot is waiting for additional information.'",
"timeoutSec": 5
}
],
"agentStop": [
{
"type": "command",
"powershell": "& \"$env:USERPROFILE\\.copilot\\hooks\\copilot-notify.ps1\" -Title 'Copilot finished' -Message 'Copilot has finished its task.'",
"timeoutSec": 5
}
]
}
}
This file is responsible only for which Copilot events invoke the notification script.
It does not decide whether the toast should be displayed.
That decision is made by copilot-notify.ps1.
7. copilot-notify.ps1
The current script is:
param(
[Parameter(Mandatory=$true)]
[string]$Title,
[Parameter(Mandatory=$true)]
[string]$Message
)
# ------------------------------------------------------------
# Configuration
# ------------------------------------------------------------
$copilotTabPrefix = "Copilot ["
# ------------------------------------------------------------
# Win32 APIs
# ------------------------------------------------------------
Add-Type @"
using System;
using System.Text;
using System.Runtime.InteropServices;
public class Win32 {
[DllImport("user32.dll")]
public static extern IntPtr GetForegroundWindow();
[DllImport("user32.dll")]
public static extern uint GetWindowThreadProcessId(
IntPtr hWnd,
out uint processId
);
}
"@
# ------------------------------------------------------------
# Get foreground window
# ------------------------------------------------------------
$foregroundHwnd = [Win32]::GetForegroundWindow()
if ($foregroundHwnd -eq [IntPtr]::Zero) {
# Detection failed -> fail open and show notification.
Import-Module BurntToast
New-BurntToastNotification -Text $Title, $Message
exit 0
}
$foregroundPid = 0
[Win32]::GetWindowThreadProcessId(
$foregroundHwnd,
[ref]$foregroundPid
) | Out-Null
$foregroundProcess = Get-Process `
-Id $foregroundPid `
-ErrorAction SilentlyContinue
# ------------------------------------------------------------
# Is the foreground application Windows Terminal?
# ------------------------------------------------------------
if (
-not $foregroundProcess -or
$foregroundProcess.ProcessName -ne "WindowsTerminal"
) {
# Chrome, Edge, VS Code, etc. is foreground.
Import-Module BurntToast
New-BurntToastNotification -Text $Title, $Message
exit 0
}
# ------------------------------------------------------------
# Determine this Copilot session's expected tab name.
#
# The Copilot launcher sets:
#
# Copilot [XXXXXXXX]
#
# using WT_SESSION.
# ------------------------------------------------------------
$session = $env:WT_SESSION
if (-not $session) {
# Not running inside Windows Terminal.
Import-Module BurntToast
New-BurntToastNotification -Text $Title, $Message
exit 0
}
$cleanSession = $session.Replace("-", "")
if ($cleanSession.Length -lt 8) {
Import-Module BurntToast
New-BurntToastNotification -Text $Title, $Message
exit 0
}
$shortSession = $cleanSession.Substring(0, 8)
$expectedTabName = "Copilot [$shortSession]"
# ------------------------------------------------------------
# Windows UI Automation
# ------------------------------------------------------------
try {
Add-Type -AssemblyName UIAutomationClient
Add-Type -AssemblyName UIAutomationTypes
$root = [System.Windows.Automation.AutomationElement]::RootElement
# Find Windows Terminal windows.
$terminalCondition =
New-Object System.Windows.Automation.PropertyCondition(
[System.Windows.Automation.AutomationElement]::ClassNameProperty,
"CASCADIA_HOSTING_WINDOW_CLASS"
)
$terminals = $root.FindAll(
[System.Windows.Automation.TreeScope]::Children,
$terminalCondition
)
$copilotTabIsSelected = $false
foreach ($terminal in $terminals) {
# Only inspect the foreground Terminal window.
if (
$terminal.Current.NativeWindowHandle -ne
$foregroundHwnd.ToInt32()
) {
continue
}
$tabCondition =
New-Object System.Windows.Automation.PropertyCondition(
[System.Windows.Automation.AutomationElement]::ControlTypeProperty,
[System.Windows.Automation.ControlType]::TabItem
)
$tabs = $terminal.FindAll(
[System.Windows.Automation.TreeScope]::Descendants,
$tabCondition
)
foreach ($tab in $tabs) {
$tabName = $tab.Current.Name
if ($tabName -ne $expectedTabName) {
continue
}
try {
$selectionPattern = $tab.GetCurrentPattern(
[System.Windows.Automation.SelectionItemPattern]::Pattern
)
if ($selectionPattern.Current.IsSelected) {
$copilotTabIsSelected = $true
}
}
catch {
# Continue; fail open below.
}
}
}
# --------------------------------------------------------
# Copilot tab is currently visible.
# Do NOT show notification.
# --------------------------------------------------------
if ($copilotTabIsSelected) {
exit 0
}
}
catch {
# UI Automation failed.
# Fail open so notifications are not silently lost.
}
# ------------------------------------------------------------
# Copilot is in the background.
# Show notification.
# ------------------------------------------------------------
Import-Module BurntToast
New-BurntToastNotification -Text $Title, $Message
8. Why the first approach failed
The original approach compared the current PowerShell/console title with the Windows Terminal window title.
That was unreliable because Windows Terminal has separate concepts for:
- the shell/profile,
- the tab title,
- the application/window title,
- and the currently selected tab.
Windows Terminal documents that tab titles may be controlled by the shell/application and that panes/tabs can have their own titles.
The failed approach effectively answered:
"Does this title equal that title?"
It did not answer:
"Is the exact Terminal tab containing this Copilot session currently selected?"
This is why a toast appeared even though the Copilot tab was visible.
9. The final tab-identification mechanism
The solution uses WT_SESSION.
Every Windows Terminal session exposes:
$env:WT_SESSION
The PowerShell wrapper takes the first eight hexadecimal characters and changes the tab title to:
Copilot [XXXXXXXX]
Example:
Copilot [3f5f2596]
The notification script runs inside that same session and reads:
$env:WT_SESSION
It reconstructs:
Copilot [3f5f2596]
It then asks Windows UI Automation for the TabItems exposed by Windows Terminal and checks SelectionItemPattern.IsSelected.
The important mapping is therefore:
Copilot process
|
+--> WT_SESSION = 3f5f2596
|
v
Copilot [3f5f2596]
|
v
Windows Terminal
TabItem.Name
|
v
IsSelected = True?
10. Foreground-window detection
The script first determines which application owns the Windows foreground window.
It uses:
GetForegroundWindow()
to get the handle of the window currently receiving user interaction.
It then uses:
GetWindowThreadProcessId()
to identify the owning process.
The result is used to distinguish:
Windows Terminal is foreground
from:
Brave/Chrome/VS Code/etc. is foreground
If a non-Terminal application is foreground, the script immediately displays the toast.
This is more precise than simply checking whether a WindowsTerminal.exe process exists.
11. Windows UI Automation
Windows UI Automation exposes Terminal tabs as TabItem controls.
The relevant properties/patterns are:
ControlType = TabItem
SelectionItemPattern.IsSelected
The diagnostic performed during development produced:
TAB: [Copilot [3f5f2596]] Selected=False
TAB: [Windows PowerShell] Selected=False
TAB: [Windows PowerShell] Selected=True
This proved that the machine exposes the selected Windows Terminal tab through UI Automation.
Microsoft's UI Automation model requires tab items to support the selection-item pattern, making IsSelected the appropriate mechanism for determining the currently selected tab.
12. PowerShell copilot wrapper
The PowerShell profile contains a wrapper named copilot.
Conceptually:
function copilot {
$session = $env:WT_SESSION
if (-not $session) {
...
}
$shortSession = $session.Replace("-", "").Substring(0, 8)
$oldTitle = $Host.UI.RawUI.WindowTitle
try {
$Host.UI.RawUI.WindowTitle = "Copilot [$shortSession]"
$realCopilot = Get-Command copilot -CommandType Application |
Select-Object -First 1
& $realCopilot.Source @args
}
finally {
$Host.UI.RawUI.WindowTitle = $oldTitle
}
}
This lets the normal workflow remain:
copilot
instead of requiring a special launcher command.
The function itself is only responsible for giving the session a stable Terminal-tab identity. It does not generate notifications.
13. Copilot terminal-title setting
Copilot CLI normally supports:
"updateTerminalTitle": true
which updates the terminal title with the current intent.
For this notification system that behavior is disabled:
"updateTerminalTitle": false
The reason is important.
We need the tab title to remain a stable identifier:
Copilot [3f5f2596]
instead of changing to:
Create Notification Test File
Analyze Repository
Implement Authentication
...
If the title changed continually, the notification script would no longer have a stable UI Automation identifier.
14. BurntToast
BurntToast is the Windows PowerShell module used to render the actual desktop toast.
The final rendering call is:
Import-Module BurntToast
New-BurntToastNotification `
-Text $Title, $Message
BurntToast was tested independently before integrating it into the hook.
A successful test produced a Windows notification in the lower-right corner.
15. Current behavior matrix
| User state when event occurs | Notification |
|---|---|
| Copilot tab is selected and visible | No |
| Another Windows Terminal tab is selected | Yes |
| Another Windows Terminal window is selected | Yes |
| Brave/Chrome is foreground | Yes |
| VS Code is foreground | Yes |
| UI Automation cannot determine the tab | Yes (fail-open) |
WT_SESSION unavailable |
Yes (fail-open) |
| Foreground-window detection fails | Yes (fail-open) |
The fail-open strategy was intentional.
A detection failure should not silently prevent an important Copilot notification.
16. Verified scenarios
The system was manually tested in three scenarios.
Scenario A — Copilot tab remains visible
A task was submitted to Copilot and the user stayed on the same Copilot tab.
Result:
No toast
Scenario B — Another Windows Terminal tab becomes active
A Copilot task was started and the user switched to another Terminal tab.
Result:
Toast displayed
Scenario C — Another application becomes active
A Copilot task was started and the user switched to Brave.
Result:
Toast displayed
These three tests confirm that the key foreground/background behavior is working.
17. Installation — the npm path
The project is distributed as the public copilot-notify npm package. The npm install and integration install are deliberately separate steps so installing the CLI does not modify Copilot configuration until the user explicitly enables the integration.
1. Install the CLI
npm install -g copilot-notify
This installs the copilot-notify command. It does not install the Copilot hooks or modify the Copilot profile by itself.
2. Install the integration
copilot-notify install
This checks the Windows prerequisites, installs the hook and PowerShell implementation, bundles the Copilot mascot, installs the stable Terminal-title wrapper, sets updateTerminalTitle to false, and records the installation state.
3. Verify and test
copilot-notify status
copilot-notify test
After copilot-notify install, start a new PowerShell/Windows Terminal session so the installed copilot wrapper is loaded, then restart Copilot CLI if it was already running.
4. Uninstall the integration
copilot-notify uninstall
This removes or restores configuration owned by Copilot Notify while preserving unrelated Copilot configuration where possible.
5. Uninstall the CLI
npm uninstall -g copilot-notify
npm install -g copilot-notify → copilot-notify install → use Copilot → copilot-notify uninstall → npm uninstall -g copilot-notify
Manual implementation details
BurntToast
Install:
Install-Module BurntToast -Scope CurrentUser
Because Windows PowerShell execution policy initially blocked the module, the current setup uses:
Set-ExecutionPolicy -Scope CurrentUser -ExecutionPolicy RemoteSigned
Verify:
Import-Module BurntToast
New-BurntToastNotification `
-Text "Copilot Test", "Toast notifications are working!"
Hook directory
New-Item `
-ItemType Directory `
-Path "$HOME\.copilot\hooks" `
-Force
Files:
$HOME\.copilot\hooks\notification-hooks.json
$HOME\.copilot\hooks\copilot-notify.ps1
If COPILOT_HOME is set, Copilot uses that location instead.
18. Restart behavior
Changes to the hook configuration are loaded when Copilot CLI starts.
Therefore:
notification-hooks.json changed
|
v
restart Copilot CLI
Changes to the PowerShell notification script itself are read when the hook fires, so modifying the .ps1 generally does not require a separate hook-config reload.
For predictable testing, restarting Copilot after configuration changes is recommended.
19. Troubleshooting
Toast appears while Copilot tab is visible
Check:
$env:WT_SESSION
Then check the Terminal tab title.
It should follow:
Copilot [XXXXXXXX]
Next run the UI Automation diagnostic and confirm:
TAB: [Copilot [XXXXXXXX]] Selected=True
If the name differs, the wrapper and notification script are no longer using the same identity.
Toast never appears
Verify BurntToast:
Import-Module BurntToast
New-BurntToastNotification `
-Text "Test", "Toast test"
Then verify the hook configuration is in:
%USERPROFILE%\.copilot\hooks\
and restart Copilot.
Copilot tab title changes unexpectedly
Check Copilot settings:
updateTerminalTitle
It should be:
"updateTerminalTitle": false
If it is true, Copilot may overwrite the stable tab identifier.
WT_SESSION is empty
Run:
$env:WT_SESSION
If it is empty, the command is not running in a Windows Terminal session or the environment has been changed.
The notification script intentionally fails open in this case.
20. Adding more notification events
To add:
shell_completed
shell_detached_completed
agent_idle
add additional entries to the notification array.
For example:
{
"type": "command",
"matcher": "shell_completed",
"powershell": "& \"$env:USERPROFILE\\.copilot\\hooks\\copilot-notify.ps1\" -Title 'Shell completed' -Message 'A background shell command has finished.'",
"timeoutSec": 5
}
The foreground/background logic does not need to change.
All new notifications automatically pass through:
notification-hooks.json
|
v
copilot-notify.ps1
|
v
foreground + selected-tab decision
21. Why this architecture is useful
This separates responsibilities cleanly.
Copilot CLI
Responsible for:
What happened?
Hook configuration
Responsible for:
Which events do we care about?
Notification script
Responsible for:
Should the user be interrupted?
BurntToast
Responsible for:
How should Windows display the notification?
This is preferable to trying to make one script understand the entire Copilot lifecycle.
22. Known limitations
This solution is practical rather than an official Copilot/Windows Terminal integration.
Windows Terminal dependency
The exact-tab logic assumes Windows Terminal and its UI Automation tree.
It is not intended to be a generic solution for every terminal emulator.
Stable-title dependency
The notification script intentionally uses the stable title:
Copilot [XXXXXXXX]
If the wrapper is removed or another process changes the tab title, the exact-tab check can fail.
The script is deliberately fail-open, so a detection failure results in a toast rather than silently losing the notification.
UI Automation dependency
UI Automation is a Windows accessibility/automation mechanism rather than a Copilot API.
A future Windows Terminal implementation could change its exposed UI Automation hierarchy, which could require adjusting the script.
Multiple panes
Windows Terminal can host multiple panes within a tab. The current implementation is designed around the Terminal tab level, not detailed pane-level focus management.
23. Rollback
To disable the custom behavior without uninstalling everything:
- Remove or rename:
%USERPROFILE%\.copilot\hooks\notification-hooks.json
- Restart Copilot.
To restore the old script:
Copy-Item `
"$HOME\.copilot\hooks\copilot-notify.ps1.bak" `
"$HOME\.copilot\hooks\copilot-notify.ps1" `
-Force
To remove BurntToast:
Uninstall-Module BurntToast
The exact Copilot hook files can also simply be deleted if the custom notification system is no longer required.
24. Current end state
The implementation is a working Windows integration and a publicly installable npm package.
npm install -g copilot-notify
copilot-notify install
Installing the CLI and installing the integration are separate operations.
copilot-notify uninstall
npm uninstall -g copilot-notify
The runtime system can be summarized as:
GitHub Copilot CLI
|
|
Native hook event
|
v
notification-hooks.json
|
v
copilot-notify.ps1
|
+-------------+-------------+
| |
Win32 foreground UI Automation
detection Terminal tabs
| |
+-------------+-------------+
|
v
Is exact Copilot tab
selected?
/ \
YES NO
| |
suppress toast
|
v
BurntToast
|
v
Windows desktop toast
This gives Copilot CLI the behavior it currently does not provide as a built-in foreground-aware Windows notification preference: notify only when attention is actually needed. The implementation is distributed as the public copilot-notify npm package so another Windows machine can install the same setup with one command.
25. References
- GitHub Copilot hooks reference: https://docs.github.com/en/copilot/reference/hooks-reference
- GitHub Copilot CLI hooks: https://docs.github.com/en/copilot/how-tos/copilot-cli/customize-copilot/use-hooks
- GitHub Copilot CLI configuration reference: https://docs.github.com/en/copilot/reference/copilot-cli-reference/cli-config-dir-reference
- Microsoft Learn — GetForegroundWindow: https://learn.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-getforegroundwindow
- Microsoft Learn — Windows Terminal command-line arguments and titles: https://learn.microsoft.com/en-us/windows/terminal/command-line-arguments
- Microsoft Learn — Windows Terminal profile/tab title settings: https://learn.microsoft.com/en-us/windows/terminal/customize-settings/profile-general
- Microsoft Learn — UI Automation TabItem: https://learn.microsoft.com/en-us/windows/win32/winauto/uiauto-supporttabitemcontroltype
- Microsoft Learn — UI Automation Tab Control: https://learn.microsoft.com/en-us/windows/win32/winauto/uiauto-supporttabcontroltype