Blog · 3 sierpnia 2026 · 11 min czytania
Agent kodujący z dostępem do shella, a ja śpię spokojnie: zabezpieczenia (guardrails), których używam
Claude Code · agenci AI · bezpieczeństwo · narzędzia deweloperskie
TL;DR: Autonomiczny agent kodujący z dostępem do shella to jednocześnie prawdziwa dźwignia i prawdziwe ryzyko. Potrafi przemielić tydzień żmudnej roboty, kiedy śpisz. Potrafi też skasować nie ten katalog, nadpisać historię force-pushem albo wypisać działający sekret do transkryptu i w każdym z tych przypadków zgłosić sukces. Chciałem agenta, który doprowadza pracę do końca i nie pyta mnie o każdy odwracalny krok, a jednocześnie fizycznie nie może zrobić tych nieodwracalnych. Potrzebowałem do tego czterech warstw: trybu uprawnień z kontrolą bezpieczeństwa, twardych reguł deny, hooka uruchamianego przed wykonaniem komendy i instrukcji dotyczącej zachowania. Pokazuję tu kształt tej konfiguracji i, co ważniejsze, powód istnienia każdej warstwy. Dokładne reguły zostają prywatne. Do przeniesienia jest rozumowanie.
Wybór, który ustawia całą resztę
Pierwsza decyzja to tryb uprawnień i właśnie tu większość ludzi się myli, bo sięga po opcję najbardziej liberalną. Tryb pełnego obejścia robi dokładnie to, co obiecuje: przestaje pytać. Problem w tym, że przestaje też pytać o zapisy w miejscach, których najmniej chcesz ruszać: w wewnętrznych plikach gita, we własnej konfiguracji agenta, w profilu shella, w danych logowania do rejestru paczek. Wszystko jest dozwolone, więc nic nie jest sprawdzane.
Zamiast tego używam trybu, który przepuszcza normalną pracę bez pytań, ale każdą akcję najpierw kieruje do klasyfikatora bezpieczeństwa działającego w tle. Odczyty, edycje plików w katalogu roboczym, instalacja zadeklarowanych zależności, sieciowe wywołania tylko do odczytu, push na branch w repo, nad którym pracujesz: to wszystko przechodzi. Ale próba podniesienia uprawnień, destrukcyjny reset, czyszczenie zasobów w chmurze, skrypt z internetu podany prosto do shella albo zapis działającego klucza tam, gdzie nigdy nie powinien trafić: to zostaje zatrzymane. Oddaję złudzenie, że "nikt mnie nigdy nie pyta", w zamian za tryb, w którym niebezpieczne kategorie ktoś faktycznie ogląda. Przy planie abonamentowym, gdzie krańcowy koszt zadania jest praktycznie zerowy, nie ma powodu wybierać tępszego narzędzia.
Warstwa druga: reguły deny, jedyna twarda gwarancja
Klasyfikator jest sprytny, ale miękki. Reguły deny są głupie i twarde, i właśnie o to chodzi. Działają w każdym trybie, nie da się ich nadpisać bardziej szczegółową regułą allow i, co najważniejsze, nie żyją w rozmowie. Granica ustawiona zdaniem "proszę, nie pushuj" żyje w transkrypcie, a transkrypt może zostać skompaktowany, kiedy kontekst robi się długi. Reguła deny to przetrwa. To jedyna gwarancja, która obowiązuje bez względu na to, co model pamięta.
Kolejność pierwszeństwa to deny, potem ask, potem allow. Wygrywa pierwsze dopasowanie, a szczegółowość reguły tej kolejności nie zmienia. Sama konfiguracja jest mała i deklaratywna: kilka wpisów, które nazywają narzędzia i formy komend, jakie nigdy nie mogą ruszyć bez nadzoru:
// tylko przykładowy kształt, nie prawdziwa lista
"permissions": {
"deny": [
"Bash(git push --force*)",
"Bash(git reset --hard*)"
// ...kategorie nieodwracalne
]
}Pisanie tych reguł dało mi kilka bolesnych lekcji składni, które warto ująć jako zasady, a nie przepisy. Blokuj całe narzędzia, nie argumenty. Wzorce, które próbują ograniczyć konkretny URL albo flagę, są kruche. Inna metoda, przekierowanie albo zmienna z tą samą wartością przechodzą obok nich bez problemu. Niektóre formy narzędzi są akceptowane, ale nigdy niczego nie dopasowują, a to gorsze niż bezużyteczne, bo martwa reguła wygląda jak ochrona. Do tego część wrapperów jest zdejmowana przed dopasowaniem, a część nie, więc regułę blokującą komendę da się obejść, uruchamiając ją przez wrapper, przez który matcher nie widzi. Lekcja nadrzędna: lista deny jest warta tyle, ile twoje empiryczne testy tej listy. I tu dochodzimy do warstwy, która istnieje właśnie dlatego, że ta ma dziurę.
Warstwa trzecia: hook, bo dopasowanie wzorców ma martwe pole
Tę lukę sprawdziłem ręcznie, a nie wyczytałem. Zablokowana komenda uruchomiona bezpośrednio zostaje zablokowana. Ta sama komenda połączona łańcuchem z inną też, bo każda podkomenda jest sprawdzana osobno. Ale opakuj ją w string dla interpretera, czyli podaj shellowi jako argument w cudzysłowie, a matcher do środka tego stringa nie zajrzy. Widzi wywołanie interpretera, uznaje je za nieszkodliwe i przepuszcza ładunek. To prawdziwe obejście, a nie hipotetyczne.
Zamyka je hook uruchamiany przed wykonaniem. Hook dostaje surowy string komendy, normalizuje go (zdejmuje cudzysłowy, zwija białe znaki) i skanuje całość, więc wrappery interpretera i zagnieżdżone cudzysłowy nie mają się gdzie schować. Kontrakt jest minimalny: kod błędu na wyjściu blokuje, a to, co hook wypisze, staje się powodem, który widzi model. Wyjście z zerem przepuszcza.
Dwie rzeczy trzymały tę warstwę w ryzach. Po pierwsze, jest celowo wąska. Blokuje tylko to, czego nie da się cofnąć: podnoszenie uprawnień, rekurencyjne kasowanie katalogu domowego albo głównego, narzędzia do czyszczenia dysków, przepisywanie historii, kasowanie zdalnego brancha, niszczenie infrastruktury, publikację paczki. Cała reszta przechodzi, bo każdy fałszywy alarm to tarcie, a tarcie to dokładnie to, co cała konfiguracja ma usuwać. Usunięcie katalogu z cache buildu musi po prostu działać. Po drugie, hook ma własny tryb awarii i ten jest naprawdę groźny: jeśli sam hook rzuci błędem, kończy się kodem zero i cała ochrona po cichu się wyłącza. Kiedyś wstawiłem flagę ignorowania wielkości liter w złym miejscu wzorca. Wzorzec rzucał błędem w runtime, a strażnik po cichu nie robił nic. Lekcja jest brutalnie prosta: warstwa bezpieczeństwa, której nigdy nie próbowałeś złamać, to nadzieja, a nie kontrola. Trzymam autotest, który odpala znane obejścia i znane fałszywe alarmy, i uruchamiam go po każdej zmianie w hooku.
Warstwa czwarta: powiedz agentowi, żeby naprawdę skończył
Uprawnienia decydują o tym, co jest możliwe, ale nie o zachowaniu. Agent może mieć techniczną zgodę na dalszą pracę, a i tak co kilka kroków zatrzymywać się z pytaniem, czy może kontynuować. Wtedy cała zabawa traci sens. Dlatego ostatnia warstwa to instrukcja, a nie mechanizm kontroli: odwracalne decyzje podejmuj sam i raportuj, co wybrałeś i dlaczego, pytaj tylko o rzeczy nieodwracalne, kosztowne albo o realną zmianę kierunku. Tryb z kontrolą bezpieczeństwa to wzmacnia, bo popycha agenta do dalszej pracy zamiast do utykania na pytaniach doprecyzowujących. Uprawnienia sprawiają, że autonomia jest bezpieczna. Ta warstwa sprawia, że jest naprawdę autonomią.
Przed czym te warstwy chronią, a przed czym nie
Warto tu być precyzyjnym, bo łatwo poczuć się bezpieczniej, niż jest naprawdę. Skasowanie całego repozytorium jest zablokowane po stronie dostawcy, jeśli token dostępu po prostu nie ma takiego uprawnienia. Żaden lokalny proces nie zrobi tego, czego token nie potrafi wyrazić. Skasowania domyślnego brancha host odmawia wprost. Ale skasowanie brancha innego niż domyślny albo przepisanie historii zatrzymuje tylko mój lokalny hook. Nie ma pod nim żadnej siatki po stronie serwera, chyba że zapłacisz za plan, który ją oferuje. Ta asymetria to uczciwy stan rzeczy: część granic gwarantuje infrastruktura, a część opiera się na skrypcie na moim laptopie, który muszę regularnie testować. Wiedza o tym, co jest czym, odróżnia prawdziwe bezpieczeństwo od poczucia bezpieczeństwa.
Co warto przenieść do siebie
- Wybieraj tryb ze sprawdzaniem zamiast obejścia. Klasyfikator, który ogląda niebezpieczne akcje, jest lepszy niż tryb, który przestaje patrzeć, zwłaszcza gdy koszt zadania jest praktycznie zerowy.
- Reguły deny to jedyna gwarancja, która przetrwa kompaktowanie kontekstu. Granicę ustaloną w rozmowie model może zapomnieć. Reguły deny nie zapomni.
- Blokuj całe narzędzia, nie argumenty. Filtry na poziomie argumentów są kruche i dają fałszywe poczucie pokrycia.
- Dodaj hook na to, czego wzorce nie widzą. Komendy opakowane w interpreter prześlizgują się przez dopasowanie. Hook, który skanuje cały znormalizowany string, je wyłapie.
- Testuj zabezpieczenia przy każdej zmianie. Warstwa bezpieczeństwa, która po cichu się otwiera, jest gorsza niż żadna, bo jej ufasz.
- Wiedz, które granice trzyma infrastruktura, a które twój laptop. Tylko jedne z nich wytrzymają, kiedy to laptop się myli.