W MiCode kilka produktów — między innymi AI Budżet i eKsięgowy AI — powstaje z agentami Claude Code i działa na jednym współdzielonym serwerze VPS. Zasady pracy z tym serwerem, z gitem i z dokumentacją projektów mieliśmy spisane: w plikach CLAUDE.md, w pamięci agenta, w runbookach. Spisane zasady mają jedną wadę: działają tylko wtedy, gdy ktoś je przeczyta w odpowiednim momencie. Claude Code ma od niedawna mody (w trybie early access) — funkcje-hooki, które zmieniają zachowanie samego agenta kodem, a nie tekstem. W jednej sesji napisaliśmy cztery takie mody, wspólne dla wszystkich naszych projektów. Ten tekst opisuje, po co, jak zostały zbudowane i czego się po nich spodziewamy.
Skąd wziął się pomysł: jeden wieczór, cztery problemy
Wszystko zaczęło się od zwykłego zadania: przejrzeć telemetrię webowej wersji AI Budżet i powiedzieć, co z niej wynika. W ciągu kilku godzin trafiliśmy na cztery rzeczy, które nie były błędami w kodzie, tylko dziurami w procesie.
Po pierwsze, agent nie mógł przeczytać danych z produkcji. Klasyfikator trybu auto słusznie zablokował zapytanie SSH do bazy, więc analiza zamieniła się w ping-pong: agent przygotowuje komendę, człowiek ją uruchamia i wkleja wynik. Pierwsza komenda od razu padła na cudzysłowach w zagnieżdżonym poleceniu SSH. Na całą analizę potrzebne były cztery takie rundy.
Po drugie, liczby mówiły coś innego, niż się wydawało. Jedno konto wewnętrzne zespołu generowało zdecydowaną większość sesji, więc lejki pokazywały głównie to, jak z aplikacji korzystamy my. Poprawka trafiła do produktu, ale lekcja była ogólna: stan produkcji trzeba widzieć na bieżąco, a nie wtedy, gdy ktoś akurat zajrzy.
Po trzecie, przy zbieraniu adresów health-checków do jednego z modów okazało się, że endpoint health jednego z naszych projektów od jakiegoś czasu odpowiadał 404. Projekt miał własny uptime-check, ale nikt nie patrzył na jego wynik.
Po czwarte, zasady. Na wspólnym serwerze jeden demon Dockera obsługuje kilka projektów, więc docker volume prune wykonany przy sprzątaniu dysku usunąłby bazy danych innych projektów. Commity mają być po angielsku, w formacie „ABA-123 Opis” bez dwukropka. Przed pushem trzeba zrobić pull, bo do gałęzi commitują też automaty. A po każdym zadaniu trzeba uzupełnić wiki projektu — ta zasada istnieje dlatego, że wiki, której nikt nie aktualizował, potrafiła twierdzić, że aplikacja ma 11 funkcji AI, kiedy miała ich 18.
Czym jest mod w Claude Code
Mod to folder z trzema plikami: manifest .claude-plugin/plugin.json, hooks/hooks.json wskazujący moduł i sam moduł w TypeScripcie, który eksportuje funkcję register. W niej wywołuje się on(zdarzenie, filtr, hook). Każdy hook ma postać ($, e, next): $ to interfejs silnika, e to dane zdarzenia, a next(e) przekazuje zdarzenie dalej — do innych pluginów i do domyślnego zachowania Claude Code. Hook, który nie wywoła next, sam odpowiada za wynik: może na przykład odmówić wywołania narzędzia.
Moduł działa w izolowanym środowisku, bez Node i bez DOM. Wszystko, co wychodzi na zewnątrz, idzie przez $: zapytania HTTP przez $.http.fetch, procesy przez $.process.run, interfejs przez $.ui.status, $.ui.toast i pasek nad Prompt to polecenie, które wpisujesz modelowi AI — od jego precyzji zależy, czy odpowiedź będzie użyteczna, czy ogólnikowa. Pełna definicja →, własne narzędzia dla modelu przez $.tool.register. Całe API to interfejs, przez który dwa systemy wymieniają dane automatycznie — bez eksportów do Excela i przeklejania ręką. Pełna definicja → jest opisane w pliku deklaracji TypeScript, który silnik generuje dla swojej wersji — w naszej to ponad 22 tysiące linii, ale wystarczy wyszukać w nim nazwę zdarzenia. Mody przeładowują się na gorąco po zapisaniu pliku, a claude plugin validate i claude plugin test sprawdzają je przed załadowaniem.
| Mod | Problem | Co robi | Oczekiwany efekt |
|---|---|---|---|
| prod-radar | Stan produkcji widać dopiero, gdy ktoś sprawdzi | Health każdego projektu w linii statusu co 90 s, pasek i powiadomienie przy awarii, dysk VPS, śledzenie deployu po git push | Awaria zauważona w ciągu 90 s pracy w sesji, deploy bez ręcznego pilnowania |
| vps-guard | Zasady wspólnego serwera i gita żyją tylko w pamięci | Odmawia niszczących komend Dockera, force-pusha na gałęzie chronione, pusha bez pull i commitów łamiących konwencję | Klasa „usunąłem cudzą bazę” niemożliwa domyślnie |
| prod-query | Agent nie może czytać produkcji, więc człowiek kopiuje komendy | Narzędzie dla modelu: jedno zapytanie SELECT w transakcji READ ONLY, każde potwierdzane przez człowieka, maskowanie danych osobowych | Analiza danych w jednym potwierdzonym kroku zamiast ping-ponga |
| finish-task | Wiedza o projekcie gnije, gdy pominie się rytuał | Po commicie z kodem bez strony wiki, wpisu w logu lub klucza zadania pokazuje przypomnienie | Dokumentacja aktualizowana w momencie commita, nie tygodnie później |
prod-radar: produkcja w linii statusu
Pierwszy mod co 90 sekund odpytuje endpointy health wszystkich projektów i pokazuje wynik w linii statusu: znaczek przy każdym projekcie i procent zajętości dysku serwera. Gdy coś pada, pojawia się czerwony pasek nad promptem i systemowe powiadomienie. Powiadamiamy tylko o zmianach stanu, żeby nie zasypać sesji komunikatami, z jednym wyjątkiem: projekt, który leży już przy pierwszym sprawdzeniu, też jest zgłaszany. Gdyby ten mod działał wcześniej, wspomniany 404 zobaczylibyśmy w pierwszej minucie sesji.
Dysk sprawdzamy co 15 minut przez SSH poleceniem df, w trybie BatchMode, więc mod nigdy nie zawiesi się na pytaniu o hasło. Ważny szczegół konfiguracji: radar patrzy na endpoint health, a nie na stronę główną. Strona główna serwowana z CDN albo statycznego kontenera nginx świeci na zielono także wtedy, gdy API za nią nie działa.
Trzecia część to obserwator deployu. Mod patrzy na każde wywołanie narzędzia Bash i gdy git push faktycznie coś wypchnął (w wyniku jest strzałka, a nie „Everything up-to-date”), co 30 sekund pyta gh run list o uruchomienia GitHub Actions dla tej gałęzi. Bierze tylko te, które powstały najwyżej minutę przed pushem, i najnowsze dla każdego workflow. Każde zakończone uruchomienie to krótki komunikat, porażka — dodatkowo powiadomienie systemowe. Po zakończeniu wszystkich mod jeszcze raz sprawdza health, bo właśnie zaraz po deployu to sprawdzenie ma największe znaczenie.
vps-guard: zasady, które nie zależą od pamięci
Drugi mod to hook na każde wywołanie Bash i PowerShell, który może odmówić wykonania komendy. Odmowa zawiera powód, a model czyta go i próbuje ponownie, dlatego każdy powód mówi nie tylko „nie”, ale też „zrób to inaczej”.
- Komenda, której agent nie wykona
- Powód, który czyta model
docker volume prune -f- Demon Dockera jest wspólny dla kilku projektów, a wolumeny to ich bazy danych. Usuń konkretny wolumen po nazwie i tylko po potwierdzeniu człowieka.
docker image prune -a -f- Nieużywane, ale otagowane obrazy to cele rollbacku innych projektów. Użyj image prune -f (tylko wiszące).
docker compose down -v- Usuwa wolumeny stosu razem z bazą. Użyj down bez -v.
git push --force origin development- Przepisuje wspólną historię. Zrób pull --rebase i zwykły push.
git push (na remote są nowe commity)- Upstream ma N commitów, których ta gałąź nie ma. Najpierw git pull --rebase.
git commit -m "ABA-12: ..."- Bez dwukropka po kluczu zadania: "ABA-12 Zrób coś".
Sprawdzenie przed pushem wykonuje git fetch i git rev-list --count HEAD..@{u}. Jeśli upstream ma commity, których lokalna gałąź nie ma, push jest odrzucany z prośbą o pull --rebase. Gdy nie ma sieci albo upstreamu, sprawdzenie przepuszcza komendę — fail open, bo blokowanie pracy przez awarię samego strażnika byłoby gorsze. Za to jeśli hook rzuci nieoczekiwany wyjątek, jego handler .catch odmawia: strażnik, który się wysypał, nie może po cichu przepuścić tego, co miał sprawdzić. Force push bez jawnie podanej gałęzi też jest odrzucany, bo z samej komendy nie widać, którą gałąź nadpisze.
To są wyrażenia regularne, a nie granica bezpieczeństwa. Chronią przed pomyłką, nie przed kimś, kto chce je obejść. Mają też fałszywe alarmy, a pierwszy złapał nas jeszcze przy pisaniu tego artykułu: skrypt, który dodawał do strony powyższą tabelę, zawierał tekst docker volume prune jako cytat, więc strażnik go odrzucił. Rozwiązanie było proste — skrypt trafił do osobnego pliku — ale dobrze pokazuje, że reguła widzi tekst komendy, a nie jej intencję.
prod-query: dostęp do produkcji, o którym decyduje człowiek
Trzeci mod rejestruje narzędzie dla modelu, prod_query: jedno zapytanie SQL to język, w którym zadaje się pytania bazie danych — to w nim zapisane jest „pokaż sprzedaż z zeszłego kwartału”. Pełna definicja → do wybranej bazy produkcyjnej. Zabezpieczenia są warstwowe. Zapytanie zawsze idzie w transakcji BEGIN TRANSACTION READ ONLY z limitem czasu 20 sekund i limitem oczekiwania na blokadę 2 sekundy i zawsze kończy się ROLLBACK. Zapytania typu SELECT są opakowane w LIMIT 500, więc limit wiersza egzekwuje sama baza.
Transakcja tylko do odczytu nie wystarcza, bo rola w kontenerze Postgresa jest superużytkownikiem. Taka rola może przez psql uruchomić powłokę poleceniem z ukośnikiem, wywołać COPY TO PROGRAM albo przeczytać plik serwera funkcją pg_read_file. Dlatego mod odrzuca ukośniki, COPY, funkcje plikowe, zdalne połączenia, funkcje sterujące serwerem, blokady wierszy i SELECT INTO, a także więcej niż jedną instrukcję w wywołaniu.
Najważniejsza warstwa to człowiek. Każde wywołanie pokazuje cel zapytania i jego treść w standardowym oknie pytania Claude Code i wykonuje się tylko po kliknięciu „Run”. Opcji „zawsze zezwalaj” nie ma. W wyniku adresy e-mail i numery telefonów w formacie międzynarodowym są maskowane, ale zwykłe ciągi cyfr nie, bo równie dobrze mogą być datą, kwotą albo identyfikatorem.
Ciekawostka z budowy: klasyfikator trybu auto zablokował zapisanie pliku z tym narzędziem z tego samego powodu, z którego wcześniej blokował zapytania SSH. Dokończyliśmy go dopiero po wyraźnej zgodzie człowieka. Tak powinno być: decyzja, czy agent dostaje dostęp do danych produkcyjnych, należy do człowieka, nie do agenta.
finish-task: przypomnienie w momencie commita
Czwarty mod obserwuje udane commity. Po każdym czyta listę zmienionych plików i sprawdza, czy commit zmienił kod (pliki źródłowe poza katalogami docs, .claude i .github). Jeśli tak, a repozytorium ma wiki, sprawdza, czy commit dotknął strony wiki i dopisał linię do logu. Sprawdza też, czy temat commita ma klucz zadania — ale tylko w repozytoriach, które takich kluczy używają. Nawyk wykrywa automatycznie: klucz musi mieć co najmniej połowa z ostatnich 20 commitów.
Gdy czegoś brakuje, nad promptem pojawia się żółty pasek z listą braków, a model dostaje kontekst z prośbą, żeby przed pushem wykonał rytuał zakończenia zadania. Mod nigdy nie blokuje commita: jest obserwatorem, więc jego błąd po prostu przepuszcza operację dalej. Skąd w ogóle wziął się ten rytuał i jak prowadzimy wiki projektu z agentem, opisaliśmy osobno.
Jak to zbudowaliśmy
Wszystkie cztery mody powstały w jednej sesji Claude Code. Agent przeczytał wygenerowany plik deklaracji API i przykłady, a potem dla każdego modu wydzielił czystą logikę do osobnego pliku: reguły strażnika, walidację SQL, klasyfikację commitów, parsowanie df i wyników gh. Dzięki temu testy działają bez sieci i bez procesów. Łącznie to około 740 linii kodu i 23 testy. Każdy mod przeszedł claude plugin validate, claude plugin test i ścisłe sprawdzenie typów tsc, a konfiguracja każdego — lista projektów, adresy, bazy — siedzi w jednym pliku config.ts. Agenci Claude Code budują u nas zresztą nie tylko narzędzia, ale też filmy reklamowe renderowane w kodzie.
Oczekiwany efekt
Mody działają od kilku godzin, więc poniżej są oczekiwania, a nie zmierzone wyniki. Wrócimy z liczbami po miesiącu.
Klasa błędów „usunąłem cudzą bazę” albo „nadpisałem cudzy commit” przestaje zależeć od pamięci i staje się niemożliwa domyślnie. Awarię zauważamy w ciągu 90 sekund pracy w sesji, a nie przy okazji. To nie zastępuje cyklicznych uptime-checków w GitHub Actions, bo radar działa tylko wtedy, gdy sesja jest otwarta. Po pushu nie trzeba ręcznie pilnować deployu. Analiza danych z produkcji to jedno potwierdzone wywołanie zamiast czterech rund kopiowania. Dokumentacja jest uzupełniana w momencie commita, a nie odkrywana jako nieaktualna tygodnie później.
Co będziemy mierzyć: liczbę odmów strażnika i odsetek fałszywych alarmów, liczbę przypomnień finish-task i to, ile z nich kończy się aktualizacją wiki, liczbę wywołań prod_query i odrzuceń, oraz czas od awarii do jej zauważenia. API modów jest we wczesnym dostępie i może się zmieniać między wersjami Claude Code, więc mody trzeba będzie czasem poprawić.
Najczęstsze pytania
- Czym jest mod w Claude Code?
- To folder z manifestem i modułem w TypeScripcie, który rejestruje hooki na zdarzenia Claude Code: wywołania narzędzi, wysłanie promptu, rysowanie interfejsu, start sesji. Hook może przepuścić zdarzenie dalej, zmienić je albo odpowiedzieć sam, na przykład odmawiając wykonania komendy. Mod może też dodać własne narzędzie dla modelu, linię statusu, pasek nad promptem albo komendę. API jest we wczesnym dostępie.
- Jak bezpiecznie dać agentowi AI odczyt produkcyjnej bazy danych?
- Warstwowo. Zapytanie idzie w transakcji READ ONLY z limitem czasu i zawsze kończy się ROLLBACK. Ponieważ rola w kontenerze bywa superużytkownikiem, osobno odrzucamy polecenia psql z ukośnikiem, COPY, funkcje plikowe i blokady. Wynik ma limit wierszy, a dane osobowe są maskowane. Najważniejsze: każde zapytanie potwierdza człowiek i nie ma opcji „zawsze zezwalaj”.
- Czy hook blokujący komendy jest zabezpieczeniem przed atakiem?
- Nie. To wyrażenia regularne na tekście komendy: chronią przed pomyłką agenta lub człowieka, a nie przed kimś, kto chce je obejść. Mają też fałszywe alarmy — nasz strażnik odrzucił skrypt, który zawierał niebezpieczną komendę tylko jako cytat w tekście artykułu. Prawdziwą granicą pozostają uprawnienia, kopie zapasowe i potwierdzenia człowieka.
- Czy mod zastępuje monitoring uptime?
- Nie. Mod działa tylko wtedy, gdy sesja Claude Code jest otwarta. Cykliczne uptime-checki w GitHub Actions z alertami na Telegram nadal działają całą dobę. Radar w sesji skraca czas zauważenia awarii podczas pracy i pokazuje stan wszystkich projektów w jednym miejscu.