# Nudge setup. Run it with Setup.bat. # Adds a Claude Code hook that sends a phone notification through ntfy.sh # whenever Claude Code stops to ask you something. param( [string]$SettingsPath = (Join-Path $env:USERPROFILE '.claude\settings.json'), [string]$KitFolder = $PSScriptRoot, [switch]$NoTest ) $ErrorActionPreference = 'Stop' function New-Topic { $chars = 'abcdefghijklmnopqrstuvwxyz0123456789' $bytes = New-Object byte[] 16 $rng = [System.Security.Cryptography.RandomNumberGenerator]::Create() $rng.GetBytes($bytes) $rng.Dispose() $s = '' foreach ($b in $bytes) { $s += $chars[$b % $chars.Length] } return 'nudge-' + $s } $marker = 'ntfy.sh/' $dir = Split-Path -Parent $SettingsPath if (-not (Test-Path -LiteralPath $dir)) { New-Item -ItemType Directory -Path $dir -Force | Out-Null } $topicFile = Join-Path $dir 'nudge-topic.txt' # Keep the same topic if Setup is run twice. if (Test-Path -LiteralPath $topicFile) { $topic = (Get-Content -LiteralPath $topicFile -Raw).Trim() } else { $topic = New-Topic [System.IO.File]::WriteAllText($topicFile, $topic, (New-Object System.Text.UTF8Encoding($false))) } $command = 'curl.exe -s -H "Title: Claude Code" -d "Needs your attention" ' + $marker + $topic # Read existing settings, or start fresh. $settings = $null if (Test-Path -LiteralPath $SettingsPath) { $raw = Get-Content -LiteralPath $SettingsPath -Raw if ($raw -and $raw.Trim().Length -gt 0) { try { $settings = $raw | ConvertFrom-Json } catch { Write-Host 'Your settings.json could not be read, so Nudge did not touch it.' Write-Host 'Nothing was changed. Fix or move that file and run Setup again.' exit 1 } Copy-Item -LiteralPath $SettingsPath -Destination ($SettingsPath + '.nudge-backup') -Force } } if ($null -eq $settings) { $settings = New-Object PSObject } if (-not ($settings.PSObject.Properties.Name -contains 'hooks')) { $settings | Add-Member -NotePropertyName 'hooks' -NotePropertyValue (New-Object PSObject) } $hooks = $settings.hooks if (-not ($hooks.PSObject.Properties.Name -contains 'Notification')) { $hooks | Add-Member -NotePropertyName 'Notification' -NotePropertyValue @() } # Drop any earlier Nudge entry, keep everything else. $keep = @() foreach ($entry in @($hooks.Notification)) { $isNudge = $false foreach ($h in @($entry.hooks)) { if ($h.command -and $h.command.ToString().Contains($marker + 'nudge-')) { $isNudge = $true } } if (-not $isNudge) { $keep += $entry } } $new = [PSCustomObject]@{ matcher = '' hooks = @([PSCustomObject]@{ type = 'command'; command = $command }) } $hooks.Notification = @($keep + $new) $json = $settings | ConvertTo-Json -Depth 20 [System.IO.File]::WriteAllText($SettingsPath, $json, (New-Object System.Text.UTF8Encoding($false))) # Make a ready-to-use chat skill with the topic filled in. $template = Join-Path $KitFolder 'chat-skill\SKILL.md' if (Test-Path -LiteralPath $template) { $out = Join-Path $KitFolder 'MY-SKILL.md' $text = (Get-Content -LiteralPath $template -Raw).Replace('YOUR-TOPIC', $topic) [System.IO.File]::WriteAllText($out, $text, (New-Object System.Text.UTF8Encoding($false))) } Write-Host '' Write-Host 'Nudge is set up.' Write-Host '' Write-Host 'Your private topic name is:' Write-Host (' ' + $topic) Write-Host 'Keep it to yourself. Anyone who knows it can send messages to your phone.' Write-Host '' Write-Host 'Next, on your phone:' Write-Host ' 1. Install the ntfy app.' Write-Host ' 2. Tap +, type the topic name above, leave "use another server" unchecked,' Write-Host ' and tick "instant delivery in doze mode". Tap Subscribe.' Write-Host ' 3. Allow notifications, and set the ntfy battery option to Unrestricted.' Write-Host '' if (-not $NoTest) { $a = Read-Host 'Subscribed? Press Enter to send a test message (or type N to skip)' if ($a -notmatch '^[Nn]') { & curl.exe -s -H 'Title: Nudge' -d 'It works. This is a test from Nudge.' ($marker + $topic) | Out-Null Write-Host 'Test sent. It should show up on your phone in a moment.' } } Write-Host '' Write-Host 'Restart Claude Code so it loads the hook.'