AI / ML 04 Wrzesień 2026

Dźwiękowe powiadomienia agenta w Cursorze (hooki)

Dostępne też w:

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-play lub ffplay (skrypt sam wybiera pierwszy dostępny).
  • jq (do parsowania payloadów hooków).
  • Zestaw dźwięków sound-theme-freedesktop w /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, a stop przychodzi dopiero po Twojej odpowiedzi. Dlatego wykrywam pytania po tekście kończącym się ?, a nie po okienku.
  • Dźwięk zgody bywa nadmiarowy. bell zagra 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ę beforeShellExecution z hooks.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.