Dźwiękowe powiadomienia agenta w Cursorze (hooki)
Jakiś czas temu opisałem globalne reguły dla Cursora dotyczące języka i interpunkcji. Dziś coś z zupełnie innej półki, ale równie „leniwie użyteczne": chcę, żeby agent dawał znać dźwiękiem, kiedy skończy pracę nad zadaniem albo kiedy o coś pyta. Dzięki temu nie muszę wpatrywać się w okno czatu i mogę w tym czasie zająć się czymś innym.
Rozwiązanie opiera się o hooki Cursora. Cały kod (instalator i skrypty hooków) trzymam w jednym snippecie, żeby ten wpis nie utonął w listingach:
➡️ Snippet z kompletem plików: bitbucket.org/LordKhayyin/.../5qp5oj
Co to daje
Trzy różne sygnały dźwiękowe:
| Kiedy | Dźwięk |
|---|---|
| Agent skończył turę (zwykłe zakończenie). | complete.oga |
Agent skończył turę zadając pytanie tekstem (wiadomość kończy się ?). |
window-question.oga |
| Akcja czeka na Twoją zgodę — komenda poza piaskownicą lub zapis pliku poza projektem. | bell.oga |
Wszystkie dźwięki to standardowe dźwięki systemowe z zestawu freedesktop, więc nie trzeba niczego dogrywać.
Dlaczego hooki, a nie „skill"
To była moja pierwsza myśl — „zróbmy skilla". Ale skill to instrukcja, którą agent czyta w trakcie pracy; nie służy do reagowania na zdarzenia cyklu życia agenta. Do „zrób coś, gdy agent skończył / gdy uruchamia komendę" służą właśnie hooki, które wymieniają JSON przez stdin/stdout i odpalają się na konkretnych zdarzeniach.
Wymagania
- Linux z działającym serwerem dźwięku (PipeWire lub PulseAudio).
- Jeden z odtwarzaczy:
paplay,pw-playlubffplay(skrypt sam wybiera pierwszy dostępny). jq(do parsowania payloadów hooków).- Zestaw dźwięków
sound-theme-freedesktopw/usr/share/sounds/freedesktop/stereo/.
Na macOS/Windows rozwiązanie trzeba by dostosować — inny odtwarzacz (afplay / powershell) i inne pliki dźwiękowe. Logika hooków pozostaje ta sama.
Instalacja
Hooki użytkownika działają globalnie, we wszystkich projektach, i mieszkają w ~/.cursor/hooks.json (rejestracja zdarzeń) oraz ~/.cursor/hooks/ (skrypty).
Najszybciej — pobierz install.sh ze snippetu i uruchom:
bash install.sh
Instalator tworzy ~/.cursor/hooks.json oraz ~/.cursor/hooks/*.sh, nadaje im prawo wykonywania i robi kopię zapasową istniejącego hooks.json. Cursor przeładowuje konfigurację automatycznie; jeśli hooki się nie pojawią, przeładuj okno (Command Palette → „Developer: Reload Window") lub sprawdź Settings → Hooks.
Jak to działa
Rozwiązanie składa się z pięciu skryptów i pliku hooks.json. Mechanizm wykrywania pytania jest ciekawszy, niż się wydaje, bo trafiłem po drodze na kilka pułapek.
Zakończenie pracy i wykrycie pytania
Nie ma zdarzenia „agent zadał pytanie". Jest za to zdarzenie stop (koniec tury). Problem: payload stop nie zawiera treści wiadomości, a czytanie transkryptu jest zawodne, bo wewnętrzne „myślenie" modelu bywa tam zapisane jako zwykły tekst i miesza się z wiadomością widoczną.
Rozwiązaniem okazało się zdarzenie afterAgentResponse, którego payload ma czyste pole text — samą widoczną wiadomość, bez rozumowania. Dlatego skrypt record-response.sh (na afterAgentResponse) zapisuje treść ostatniej wiadomości do pliku stanu, a notify-stop.sh (na stop) odczytuje ją, sprawdza, czy kończy się znakiem ?, i wybiera dźwięk „pytanie" albo „gotowe". Takie rozdzielenie odpornie radzi sobie z opóźnieniem transkryptu i z myśleniem-jako-tekstem.
Dźwięk przy prośbie o zgodę (komendy)
Zdarzenie beforeShellExecution ma w payloadzie pole sandbox. Gdy jest false, komenda uruchamia się poza piaskownicą i Cursor pyta o zgodę — wtedy approval-notify.sh gra bell. Gdy true, komenda idzie do piaskownicy bez pytania i panuje cisza.
Uwaga na pułapkę jq: operator // traktuje wartość false tak samo jak null, więc .sandbox // "true" dla sandbox:false zwróciłby "true" i odwróciłby logikę. Trzeba czytać surową wartość przez jq -r '.sandbox'.
Dźwięk przy prośbie o zgodę (zapis plików)
Narzędzia plikowe nie mają flagi zgody, ale przechodzą przez preToolUse. W praktyce Cursor wstrzymuje na zgodę zapisy poza katalogami projektu. Dlatego file-approval-notify.sh porównuje ścieżkę docelową z katalogami workspace i gra bell, gdy zapis celuje poza projekt. To zdarzenie odpala się przed oknem zgody, więc dźwięk pojawia się w dobrym momencie.
Ograniczenia (świadome kompromisy)
- Okienko pytania z opcjami (
AskQuestion) nie da dźwięku. To interaktywne narzędzie nie emituje żadnego hooka, astopprzychodzi dopiero po Twojej odpowiedzi. Dlatego wykrywam pytania po tekście kończącym się?, a nie po okienku. - Dźwięk zgody bywa nadmiarowy.
bellzagra przy każdej komendzie poza piaskownicą i przy każdym zapisie poza projektem — także wtedy, gdy dana ścieżka lub komenda jest na Twojej allowliście i uruchamia się bez pytania. Payloady nie zawierają pola, które odróżniłoby „auto-akceptację" od „realnej prośby o zgodę". - Odczyty plików pomijam. Odczyty zwykle nie wstrzymują akcji, więc granie na nich byłoby tylko hałasem.
Dostosowanie
- Inne dźwięki — podmień nazwę w argumencie wywołania
play-sound.sh(np.bell→dialog-warning). Dostępne nazwy znajdziesz w/usr/share/sounds/freedesktop/stereo/. - Wyłączenie sygnału zgody dla komend — usuń sekcję
beforeShellExecutionzhooks.json. - Wyłączenie sygnału zgody dla plików — usuń sekcję
preToolUse.
Podsumowanie
Kilka niewielkich skryptów i jeden plik konfiguracyjny sprawiają, że agent przestaje pracować „w ciszy": słyszę, gdy skończył, gdy o coś pyta i gdy czeka na moją zgodę. Reszta uwagi zostaje wolna na inne rzeczy. Komplet plików jest w snippecie na Bitbucket.