Skip to content

Instantly share code, notes, and snippets.

@ChrisColeTech
Last active August 18, 2025 07:38
Show Gist options
  • Select an option

  • Save ChrisColeTech/1f79919c60bf210982a04bc607a1a1d7 to your computer and use it in GitHub Desktop.

Select an option

Save ChrisColeTech/1f79919c60bf210982a04bc607a1a1d7 to your computer and use it in GitHub Desktop.
Windows Toast Notification Installer - Native Windows notifications (WORKING)
# Windows Toast Notification Installer
# This script sets up toast notifications for Windows using the BurntToast module
# Usage: Run this script in PowerShell as Administrator (recommended) or current user
param(
[switch]$Unattended = $false
)
# Function to write colored output
function Write-ColorOutput($ForegroundColor) {
$fc = $host.UI.RawUI.ForegroundColor
$host.UI.RawUI.ForegroundColor = $ForegroundColor
if ($args) {
Write-Output $args
} else {
$input | Write-Output
}
$host.UI.RawUI.ForegroundColor = $fc
}
Write-ColorOutput Green "=== Windows Toast Notification Installer ==="
Write-Output ""
# Cleanup old installations
Write-Output "Checking for old installations to clean up..."
# Clean up any user-specific PATH entries that contain toast
$currentUserPath = [Environment]::GetEnvironmentVariable("PATH", "User")
if ($currentUserPath) {
$pathParts = $currentUserPath -split ';'
$cleanedParts = @()
$removedPaths = @()
foreach ($pathPart in $pathParts) {
if ($pathPart) {
# Check if this path contains old toast installations
$checkPath = $pathPart.TrimEnd('\')
if ((Test-Path "$checkPath\toast.ps1") -or
(Test-Path "$checkPath\toast.cmd") -or
(Test-Path "$checkPath\toast")) {
# Skip paths that aren't our target ProgramData location
if ($checkPath -ne "C:\ProgramData\Toast") {
$removedPaths += $checkPath
Write-Output "Removing old toast files from: $checkPath"
Remove-Item -Path "$checkPath\toast.ps1" -Force -ErrorAction SilentlyContinue
Remove-Item -Path "$checkPath\toast.cmd" -Force -ErrorAction SilentlyContinue
Remove-Item -Path "$checkPath\toast" -Force -ErrorAction SilentlyContinue
continue
}
}
$cleanedParts += $pathPart
}
}
if ($removedPaths.Count -gt 0) {
Write-Output "Removed toast installations from PATH: $($removedPaths -join ', ')"
$cleanedPath = $cleanedParts -join ';'
[Environment]::SetEnvironmentVariable("PATH", $cleanedPath, "User")
$env:PATH = [Environment]::GetEnvironmentVariable("PATH", "Machine") + ";" + $cleanedPath
}
}
# Clean up bash profile toast functions if they exist
$bashProfiles = @(
"$env:USERPROFILE\.bashrc",
"$env:USERPROFILE\.bash_profile",
"$env:USERPROFILE\.profile"
)
foreach ($profilePath in $bashProfiles) {
if (Test-Path $profilePath) {
$content = Get-Content $profilePath -Raw
if ($content -like "*toast*helper*" -or $content -like "*toast()*") {
Write-Output "Cleaning up old bash toast functions from: $profilePath"
# Remove toast helper blocks
$cleaned = $content -replace '(?s)# -- .*?toast.*?helper start --.*?# -- .*?toast.*?helper end --\r?\n?', ''
# Remove standalone toast functions
$cleaned = $cleaned -replace '(?s)toast\(\)\s*\{[^}]*\}\r?\n?', ''
if ($cleaned -ne $content) {
Set-Content -Path $profilePath -Value $cleaned -Encoding UTF8 -NoNewline
}
}
}
}
Write-Output "Cleanup complete."
Write-Output ""
# Create global toast directory in ProgramData
$binDir = "C:\ProgramData\Toast"
if (-not (Test-Path $binDir)) {
Write-Output "Creating global toast directory: $binDir"
New-Item -ItemType Directory -Path $binDir -Force | Out-Null
}
# Download the icon image
$iconPath = Join-Path $binDir "app-icon.png"
if (-not (Test-Path $iconPath)) {
Write-Output "Downloading toast notification icon..."
try {
$imageUrl = 'https://github.com/ChrisColeTech/claude-wrapper/raw/main/app-1.png'
Invoke-WebRequest -Uri $imageUrl -OutFile $iconPath
Write-Output "Icon downloaded successfully"
} catch {
Write-Warning "Failed to download icon: $_"
$iconPath = $null
}
} else {
Write-Output "Icon already exists"
}
# Create toast.ps1 script
$toastPs1Path = Join-Path $binDir "toast.ps1"
$toastPs1Content = @"
Param(
[Parameter(Position=0)] [string]`$Title = "Notification",
[Parameter(Position=1)] [string]`$Body = ""
)
# Ensure module available per-user (non-fatal if blocked)
if (-not (Get-Module -ListAvailable -Name BurntToast)) {
try {
Write-Host "Installing BurntToast module..."
Install-Module -Name BurntToast -Scope CurrentUser -Force -ErrorAction Stop
} catch {
Write-Warning "Failed to install BurntToast module: `$_"
exit 1
}
}
Import-Module BurntToast -ErrorAction SilentlyContinue
if (-not (Get-Module BurntToast)) {
Write-Error "BurntToast module not available"
exit 1
}
`$text = @(`$Title)
if (`$Body -ne '') { `$text += `$Body }
try {
# Use custom icon if available
`$iconPath = "C:\ProgramData\Toast\app-icon.png"
if (Test-Path `$iconPath) {
`$logo = New-BTImage -Source `$iconPath
New-BurntToastNotification -Text `$text -AppLogo `$logo
} else {
New-BurntToastNotification -Text `$text
}
} catch {
Write-Error "Failed to create toast notification: `$_"
exit 1
}
"@
Write-Output "Creating PowerShell script: $toastPs1Path"
Set-Content -Path $toastPs1Path -Value $toastPs1Content -Encoding UTF8
# Create toast.cmd wrapper with full PowerShell path
$toastCmdPath = Join-Path $binDir "toast.cmd"
$toastCmdContent = @'
@echo off
"C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe" -ExecutionPolicy Bypass -File "C:\ProgramData\Toast\toast.ps1" %*
'@
Write-Output "Creating CMD wrapper: $toastCmdPath"
Set-Content -Path $toastCmdPath -Value $toastCmdContent -Encoding ASCII
# Add bin directory to PATH if not already present
$currentPath = [Environment]::GetEnvironmentVariable("PATH", "User")
if ($currentPath -notlike "*$binDir*") {
Write-Output "Adding $binDir to user PATH..."
$newPath = "$currentPath;$binDir"
[Environment]::SetEnvironmentVariable("PATH", $newPath, "User")
$env:PATH = "$env:PATH;$binDir"
} else {
Write-Output "Bin directory already in PATH"
}
# Update PowerShell profile
$profilePath = $PROFILE.CurrentUserAllHosts
$profileDir = Split-Path $profilePath -Parent
if (-not (Test-Path $profileDir)) {
Write-Output "Creating PowerShell profile directory: $profileDir"
New-Item -ItemType Directory -Path $profileDir -Force | Out-Null
}
$profileSnippet = @'
# Toast notification functions
if (-not (Get-Command toast -ErrorAction SilentlyContinue)) {
function Show-Toast {
param([string]$Title = "Notification", [string]$Body = "")
& "C:\ProgramData\Toast\toast.ps1" $Title $Body
}
Set-Alias -Name toast -Value Show-Toast
Set-Alias -Name t -Value Show-Toast
}
'@
$profileExists = Test-Path $profilePath
if ($profileExists) {
Write-Output "Checking for existing toast functions in PowerShell profile..."
$profileContent = Get-Content $profilePath -Raw
# Remove any existing toast function blocks (clean update)
if ($profileContent -like "*Toast notification functions*") {
Write-Output "Removing old toast functions from PowerShell profile..."
# Use regex to remove the entire toast function block
$pattern = '(?s)# Toast notification functions.*?(?=\r?\n\r?\n(?![\s]*[}]|[\s]*Set-Alias|[\s]*function)|\z)'
$cleanedContent = $profileContent -replace $pattern, ''
# Clean up any extra blank lines
$cleanedContent = $cleanedContent -replace '\r?\n\r?\n\r?\n+', "`r`n`r`n"
# Write the cleaned content back to profile
Set-Content -Path $profilePath -Value $cleanedContent -Encoding UTF8 -NoNewline
}
}
Write-Output "Adding updated toast functions to PowerShell profile: $profilePath"
Add-Content -Path $profilePath -Value $profileSnippet -Encoding UTF8
# Test installation
Write-Output ""
Write-ColorOutput Green "Installation complete!"
Write-Output ""
Write-Output "Testing installation..."
try {
& $toastPs1Path "Installation Complete" "Windows toast notifications with custom icon are now available"
Write-ColorOutput Green "[SUCCESS] Test notification sent successfully!"
} catch {
Write-ColorOutput Red "[ERROR] Test notification failed: $_"
}
# Configure Claude Code hooks
Write-Output ""
Write-ColorOutput Green "Configuring Claude Code hooks..."
$claudeSettingsDir = "$env:USERPROFILE\.claude"
$claudeSettingsPath = "$claudeSettingsDir\settings.json"
# Important warning about hooks replacement
Write-ColorOutput Yellow "IMPORTANT: This installer will replace any existing Claude Code hooks"
Write-ColorOutput Yellow "with toast notification hooks for optimal functionality."
Write-Output ""
if (-not (Test-Path $claudeSettingsDir)) {
Write-Output "Creating Claude settings directory: $claudeSettingsDir"
New-Item -ItemType Directory -Path $claudeSettingsDir -Force | Out-Null
}
$criticalHooks = @{
SessionStart = @(
@{
matcher = "startup"
hooks = @(
@{
type = "command"
command = 'toast "Claude Code" "New session started - Ready to help!"'
}
)
},
@{
matcher = "resume"
hooks = @(
@{
type = "command"
command = 'toast "Claude Code" "Session resumed - Continuing where we left off"'
}
)
}
)
Notification = @(
@{
hooks = @(
@{
type = "command"
command = 'toast "Claude Code" "User input needed to continue"'
}
)
}
)
Stop = @(
@{
hooks = @(
@{
type = "command"
command = 'toast "Claude Code" "Task completed successfully"'
}
)
}
)
SubagentStop = @(
@{
hooks = @(
@{
type = "command"
command = 'toast "Claude Code" "Subagent task finished"'
}
)
}
)
PreCompact = @(
@{
hooks = @(
@{
type = "command"
command = 'toast "Claude Code" "Compacting context to continue"'
}
)
}
)
}
$settings = @{}
if (Test-Path $claudeSettingsPath) {
Write-Output "Found existing Claude Code settings file"
# Create timestamped backup
$timestamp = Get-Date -Format "yyyyMMdd_HHmmss"
$backupPath = "$claudeSettingsPath.backup_$timestamp"
try {
Copy-Item $claudeSettingsPath $backupPath -Force
Write-ColorOutput Green "Created backup: $backupPath"
} catch {
Write-Warning "Failed to create backup: $_"
}
# Read existing settings
try {
$existingJson = Get-Content $claudeSettingsPath -Raw | ConvertFrom-Json
# Convert PSCustomObject to hashtable while preserving all properties
$settings = @{}
$existingJson.PSObject.Properties | ForEach-Object {
$settings[$_.Name] = $_.Value
}
Write-Output "Preserved existing settings: $($settings.Keys -join ', ')"
} catch {
Write-Warning "Could not parse existing settings, creating new configuration"
$settings = @{}
}
} else {
Write-Output "Creating new Claude settings file..."
}
# Create clean JSON manually to ensure correct format
Write-Output "Updating Claude settings with critical hooks..."
# Build settings preserving existing configuration
$finalSettings = @{}
foreach ($key in $settings.Keys) {
if ($key -ne "hooks") {
$finalSettings[$key] = $settings[$key]
}
}
# Build settings preserving existing configuration and create clean JSON manually
$existingSettingsJson = ""
foreach ($key in $finalSettings.Keys) {
if ($key -ne "hooks") {
$value = $finalSettings[$key]
if ($value -is [string]) {
$existingSettingsJson += " `"$key`": `"$value`",`n"
} else {
$existingSettingsJson += " `"$key`": $($value | ConvertTo-Json -Compress),`n"
}
}
}
# Create the JSON manually to avoid encoding issues with emojis
$hooksJsonContent = @"
{
$existingSettingsJson "hooks": {
"SessionStart": [
{
"matcher": "startup",
"hooks": [
{
"type": "command",
"command": "toast \"Claude Code\" \"New session started - Ready to help!\""
}
]
},
{
"matcher": "resume",
"hooks": [
{
"type": "command",
"command": "toast \"Claude Code\" \"Session resumed - Continuing where we left off\""
}
]
}
],
"Notification": [
{
"hooks": [
{
"type": "command",
"command": "toast \"Claude Code\" \"User input needed to continue\""
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"command": "toast \"Claude Code\" \"Task completed successfully\""
}
]
}
],
"SubagentStop": [
{
"hooks": [
{
"type": "command",
"command": "toast \"Claude Code\" \"Subagent task finished\""
}
]
}
],
"PreCompact": [
{
"hooks": [
{
"type": "command",
"command": "toast \"Claude Code\" \"Compacting context to continue\""
}
]
}
]
}
}
"@
# Write with proper UTF-8 encoding
$utf8NoBomEncoding = New-Object System.Text.UTF8Encoding $False
[System.IO.File]::WriteAllText($claudeSettingsPath, $hooksJsonContent, $utf8NoBomEncoding)
Write-ColorOutput Green "Critical hooks configured successfully!"
Write-Output ""
Write-Output "Usage examples:"
Write-Output ' toast "Hello" "World"'
Write-Output ' t "Quick notification"'
Write-Output ""
Write-Output "Note: You may need to restart your terminal or run 'refreshenv' to use the toast command."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment