Kiedy pierwszy raz wpuściłem Claude Code do prawdziwego projektu .NET, potraktowałem go jak juniora. Zadanie było konkretne: system ról i uprawnień w ASP.NET Core. Znałem tę robotę, robiłem ją wcześniej kilka razy i wiedziałem, że zajmie mi 2 dni. Zajęła 3 godziny 10 minut.
To nie jest jednak kolejny tekst w stylu "AI jest szybkie". Po drodze AI zrobiło 3 błędy. Jeden z nich dałby zwolnionemu pracownikowi pełny dostęp admina jeszcze przez godzinę po zwolnieniu. I druga ciekawa rzecz: najwięcej czasu spędziłem, zanim AI napisało pierwszą linijkę kodu.
W tym artykule pokażę Ci całą oś czasu tego taska, minuta po minucie. Zobaczysz, co było w pliku SPEC.md, który dałem AI, jakie błędy zrobił Claude Code i jak je wyłapałem. A na koniec uczciwie: co z tego powtórzysz jutro w swoim projekcie, a co było zwykłym szczęściem. Przepis jest na samym końcu, ale bez części o błędach AI nie zadziała.
Stack w tym tasku: ASP.NET Core z JWT i policy-based authorization, EF Core z migracjami, testy integracyjne na WebApplicationFactory i Claude Code w roli juniora, który pisze kod według moich ustaleń.
Zakres i estymacja: skąd 2 dni?
Zacznijmy od zakresu, bo bez niego te liczby nic nie znaczą.
Zadanie. Typowe, z gatunku tych, które w firmowych projektach robi się co jakiś czas. Składało się z 3 rzeczy:
• JWT. Użytkownik się loguje i dostaje token.
• Policy-based authorization. Endpoint chroni polityka, na przykład "może zarządzać użytkownikami", a nie sztywna nazwa roli.
• Role w bazie. Admin zmienia uprawnienia w aplikacji, bez deployu.
Ostatni punkt jest kluczowy dla całej tej historii. Skoro role i ich uprawnienia żyją w bazie, to baza jest źródłem prawdy. Zapamiętaj to, bo wrócimy do tego przy błędzie numer 2.
W zakresie i poza zakresem. W zakresie były: tabele ról i uprawnień z migracją, role startowe, polityki, API do zarządzania rolami, zabezpieczenie istniejących endpointów i testy. Poza zakresem: panel w UI, uprawnienia do pojedynczych rekordów i logowanie przez zewnętrznego dostawcę. To "poza zakresem" jest ważniejsze, niż myślisz. Wrócę do niego przy SPEC.md.
Skąd 2 dni? Estymacja nie była strzałem z sufitu, tylko rozpisanym planem:
• Dzień 1 (ok. 8 h): model danych i migracja, JWT i polityki, API do ról.
• Dzień 2 (ok. 8 h): zabezpieczenie istniejących endpointów, testy i poprawki.
Razem około 16 godzin pracy.
Znana robota = dobry test. Robiłem takie moduły kilka razy, więc ten task był dobrym testem: znana robota i znany czas. Gdybym wziął coś zupełnie nowego, nie wiedziałbym, czy różnica wynika z AI, czy z tego, że po prostu źle oszacowałem. Tu porównanie było uczciwe. Jeśli AI miało gdzieś pokazać różnicę, to właśnie tu.
Ty jesteś tech leadem, Claude Code juniorem. Co zmieniłem w porównaniu z typowym "pogadaj z AI"? Nie napisałem "zrób role". Podzieliłem pracę jak w zespole:
• Ja = tech lead: decyzje, specyfikacja, review.
• Claude Code = junior: pisze kod według moich ustaleń, generuje szkielety i testy z mojej listy.
Ten podział przewija się przez cały task. Wszystko, co wymaga myślenia o konsekwencjach, zostało po mojej stronie.
Oś czasu: 190 minut w 5 etapach
Całość zajęła 190 minut, czyli 3 h 10 min, podzielone na 5 etapów:
| Etap | Czas | Co robiłem ja | Co robił Claude Code |
| 00:00-00:35 SPEC.md i konwencje | 35 min | pisałem specyfikację, wskazałem wzorcowy moduł | nic - ani linijki kodu |
| 00:35-00:55 Szkielet | 20 min | przegląd planu i akceptacja | plan, potem encje, EF Core, migracja, seed ról, polityki, handler, kontroler |
| 00:55-01:55 Przypadki brzegowe | 60 min | decyzja, jak ma działać każdy przypadek | implementacja |
| 01:55-02:40 Testy | 45 min | macierz 401 / 403 / 2xx, sprawdzanie, czy testy umieją się wywalić | testy integracyjne na WebApplicationFactory |
| 02:40-03:10 Review i commit | 30 min | diff plik po pliku, porównanie ze SPEC, definition of done | - |
Teraz po kolei.
00:00-00:35: SPEC.md i konwencje. Przez pierwsze 35 minut AI nie napisało ani jednej linijki kodu. Ja pisałem SPEC.md: cel, zakres, decyzje, konwencje, przypadki brzegowe i definition of done. Do tego wskazałem moduł, na którym AI ma się wzorować. Większość ludzi pomija ten krok. A potem narzeka, że AI robi, co chce. Co dokładnie było w tym pliku, pokażę w osobnej sekcji, bo to on zrobił największą różnicę.
00:35-00:55: szkielet w 20 minut. W 35. minucie kazałem Claude Code przeczytać SPEC i najpierw pokazać plan, bez ruszania plików. Przejrzałem plan i puściłem. 20 minut później miałem szkielet: encje, konfigurację EF Core, migrację, seed ról, polityki, handler autoryzacji i kontroler do ról. Małymi krokami, z commitem po każdym.
Tak w uproszczeniu wygląda rejestracja polityk. Każde uprawnienie to stała w klasie Permissions i osobna polityka. Zero magicznych stringów:
public static class Permissions
{
public const string UsersManage = "users.manage";
public const string RolesManage = "roles.manage";
public static readonly string[] All = { UsersManage, RolesManage };
}
/* Program.cs tylko rejestruje */
builder.Services.AddAuthorization(options =>
{
foreach (var permission in Permissions.All)
{
options.AddPolicy(permission, policy =>
policy.Requirements.Add(new PermissionRequirement(permission)));
}
});
builder.Services.AddScoped<IAuthorizationHandler, PermissionHandler>();
/* endpoint chroniony polityką, a nie nazwą roli */
[Authorize(Policy = Permissions.UsersManage)]
[HttpDelete("{id}")]
public async Task<IActionResult> DeleteUser(Guid id) { /* ... */ }To szło prosto ze SPEC, więc AI trzymało się tego... prawie wszędzie. O tym "prawie" za chwilę.
00:55-01:55: przypadki brzegowe. Najdłuższy etap, cała godzina. Lista wyglądała tak:
• brak tokena,
• użytkownik bez uprawnienia,
• odebranie roli w trakcie sesji,
• usunięcie roli, którą ktoś ma,
• ostatni admin, który odbiera sobie admina,
• duplikat nazwy roli.
Zapamiętaj trzeci punkt, z odebraniem roli w trakcie sesji. Za chwilę okaże się najważniejszy na całej liście. Przy każdym przypadku to ja decydowałem, jak ma działać, a AI to implementowało. Proporcje mówią same za siebie: szkielet 20 minut, dopracowanie 3 razy tyle.
01:55-02:40: testy, czyli macierz 401 / 403 / 2xx. Godzina 1:55. Najbardziej przydała się prosta macierz: każdy chroniony endpoint razy 3 sytuacje:
• bez tokena: 401 (Unauthorized),
• rola bez uprawnienia: 403 (Forbidden),
• z uprawnieniem: sukces (2xx).
Z tej macierzy Claude Code wygenerował testy integracyjne na WebApplicationFactory. Moja rola w tym etapie była inna, niż mogłoby się wydawać: nie pisałem testów, tylko sprawdzałem, czy te testy w ogóle potrafią się wywalić. Do tego jeszcze wrócę, bo to klucz do błędu numer 3.
02:40-03:10: review i commit. Ostatnie 30 minut to review, dokładnie takie samo jak u juniora:
• diff plik po pliku,
• szukanie stringów zamiast stałych,
• porównanie ze SPEC,
• sprawdzenie definition of done,
• commit.
3 godziny 10 minut i task gotowy.
Gdzie poszedł czas? I gdzie zniknęło prawie 13 godzin
A teraz najciekawsze. Szkielet, czyli to, co widzisz w każdym demo AI, to około 10% całości (20 ze 190 minut). Reszta to specyfikacja, przypadki brzegowe, testy i review. Innymi słowy: myślenie i sprawdzanie.
Estymacja mówiła 16 godzin, wyszło 3 h 10 min. To gdzie zniknęło prawie 13 godzin?
Co zniknęło:
• pisanie boilerplate'u,
• konfiguracja,
• szkielety testów,
• szukanie w dokumentacji, jak coś zarejestrować.
Co nie zniknęło:
• decyzje,
• przypadki brzegowe,
• review.
Kodu wcale nie było mniej. Pisałem go inaczej: zamiast klepać go ręcznie, opisywałem, co ma powstać, i sprawdzałem, co powstało. Jeśli więc ktoś mówi Ci, że AI "pisze kod za programistę", to mówi tylko o tych 10%.
Co było w SPEC.md, czyli plik, który zrobił różnicę
SPEC.md miał 6 części i około 2 stron:
1. Cel.
2. Zakres i poza zakresem.
3. Decyzje.
4. Konwencje.
5. Przypadki brzegowe.
6. Definition of done.
Przejdźmy przez nie po kolei.
Cel i poza zakresem. Cel: 1 zdanie. Poza zakresem: wprost, punkt po punkcie. Bez tego AI chętnie zbuduje Ci panel admina, którego nie chciałeś. Junior robi dokładnie to samo, jeśli nikt mu nie powie, gdzie jest granica. Dlatego "poza zakresem" z początku artykułu było takie ważne. To nie formalność, tylko płot, który trzyma AI w ryzach.
Decyzje. Najważniejsza część pliku. U mnie wyglądała tak:
• autoryzacja tylko przez polityki,
• źródło prawdy o rolach: baza,
• bez tokena 401, bez uprawnienia 403,
• token niesie tylko tożsamość, uprawnienia zawsze z bazy.
Każdą z tych decyzji podjąłem ja, nie AI. Ostatnią dopisałem zresztą w trakcie, po błędzie numer 2. To ważna lekcja: SPEC żyje razem z taskiem. Kiedy w trakcie pracy podejmujesz nową decyzję, dopisujesz ją do pliku, żeby AI (i Ty za tydzień) miało ją przed oczami.
Konwencje. Konwencji nie opisywałem esejem. Wskazałem wzorzec i dodałem kilka twardych zasad:
• rób jak w Features/Orders,
• uprawnienia jako stałe,
• Program.cs tylko rejestruje,
• nowa paczka NuGet: najpierw pytasz.
AI dużo lepiej kopiuje wzorzec, niż czyta opis. Zamiast pisać akapity o strukturze folderów, nazewnictwie i rejestracji zależności, wskaż istniejący moduł, który robi to dobrze.
Przypadki brzegowe i definition of done. Przypadki brzegowe spisałem przed kodem, nie po. Każdy z nich zamienił się potem w test. A definition of done sprawdzałem na końcu:
• build i testy zielone,
• każdy endpoint ma politykę,
• migrację da się cofnąć.
Szablon SPEC.md. Złożony w całość, taki plik może wyglądać mniej więcej tak (to skrócony szkielet, który możesz skopiować i wypełnić pod swój task):
# SPEC: role i uprawnienia
## Cel
Role i uprawnienia w bazie, endpointy chronione politykami - admin zmienia uprawnienia bez deployu.
## Zakres
- tabele ról i uprawnień + migracja, role startowe
- polityki, API do zarządzania rolami
- zabezpieczenie istniejących endpointów, testy
## Poza zakresem
- panel w UI
- uprawnienia do pojedynczych rekordów
- logowanie przez zewnętrznego dostawcę
## Decyzje
- autoryzacja tylko przez polityki
- źródło prawdy o rolach: baza
- bez tokena 401, bez uprawnienia 403
- token niesie tylko tożsamość, uprawnienia z bazy (dopisane po błędzie #2)
## Konwencje
- rób jak w Features/Orders
- uprawnienia jako stałe w klasie Permissions
- Program.cs tylko rejestruje
- nowa paczka NuGet: najpierw pytasz
## Przypadki brzegowe
- brak tokena
- użytkownik bez uprawnienia
- odebranie roli w trakcie sesji: działa od następnego requestu
- usunięcie roli, którą ktoś ma
- ostatni admin, który odbiera sobie admina
- duplikat nazwy roli
## Definition of done
- build i testy zielone
- każdy endpoint ma politykę
- migrację da się cofnąćSłabe vs dobre polecenie. Porównaj 2 polecenia. Słabe:
"Dodaj system ról z JWT."
Przy takim poleceniu AI zgaduje wszystko: strukturę, nazwy, zakres i sposób autoryzacji. Dobre:
"Przeczytaj SPEC.md i CLAUDE.md. Wzoruj się na Features/Orders. Zrób tylko krok 1. Najpierw pokaż plan. Jeśli coś jest niejasne - pytaj."
Tak dajesz zadanie juniorowi. I dokładnie tak samo dajesz je AI.
3 błędy AI (jeden groźny)
Teraz część, bez której przepis z końca artykułu nie zadziała. AI zrobiło 3 błędy. Jeden z nich był naprawdę groźny.
Błąd #1: rola jako string. Najmniejszy. W SPEC stało: tylko polityki. A w jednym kontrolerze AI wstawiło atrybut z rolą wpisaną na sztywno:
/* tak zrobiło AI - omija cały system uprawnień */
[Authorize(Roles = "Admin")]
/* tak mówił SPEC */
[Authorize(Policy = Permissions.RolesManage)]Działa? Działa. Ale omija cały system uprawnień. Admin zmieni uprawnienia roli w bazie, a ten endpoint i tak patrzy tylko na nazwę roli. Testy z macierzy mogą tego nie wyłapać, bo admin i tak dostaje sukces, a rola bez uprawnienia dostaje 403. Wyłapałem to w review prostym sposobem: szukam w diffie "Roles =".
Błąd #2: zwolniony, a dalej admin. Ten groźny. Zobacz scenariusz:
• 10:00 - pracownik loguje się jako admin,
• 10:05 - odbierasz mu uprawnienia, bo właśnie został zwolniony,
• 10:59 - on dalej może usuwać użytkowników.
Dlaczego? AI przy logowaniu wpisało role do tokena JWT. Token był ważny godzinę. Baza mówi "nie jest adminem", token mówi "jest". A aplikacja wierzy tokenowi. Czyli dokładnie wbrew decyzji, że źródłem prawdy o rolach jest baza. Role w claimach tokena to częsty wzorzec w przykładach z internetu, więc nic dziwnego, że AI po niego sięgnęło. Problem w tym, że kłóci się z wymaganiem "admin zmienia uprawnienia bez deployu".
Wyłapałem to, bo w SPEC miałem przypadek brzegowy: odebranie roli działa od następnego requestu. Z tego przypadku powstał test. I ten test był czerwony. W uproszczeniu wyglądał tak:
[Fact]
public async Task RevokedRole_TakesEffectOnNextRequest()
{
var client = await CreateClientAsAsync("Admin"); /* token ważny godzinę */
await RevokeRoleAsync(AdminUserId, "Admin");
var response = await client.DeleteAsync($"/api/users/{OtherUserId}");
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
}Poprawka: token niesie tylko tożsamość. Handler autoryzacji sprawdza uprawnienia w bazie (przez cache, żeby nie odpytywać bazy przy każdym requeście), a każda zmiana ról czyści ten cache. W uproszczeniu:
public class PermissionHandler : AuthorizationHandler<PermissionRequirement>
{
private readonly IPermissionService _permissions;
public PermissionHandler(IPermissionService permissions) => _permissions = permissions;
protected override async Task HandleRequirementAsync(
AuthorizationHandlerContext context, PermissionRequirement requirement)
{
var userId = context.User.FindFirstValue(ClaimTypes.NameIdentifier);
if (userId is null)
return;
/* uprawnienia z bazy (przez cache), nie z tokena */
var permissions = await _permissions.GetForUserAsync(userId);
if (permissions.Contains(requirement.Permission))
context.Succeed(requirement);
}
}Gdybym nie spisał tego przypadku brzegowego, nie byłoby testu. A ten błąd pojechałby na produkcję.
Błąd #3: test zielony ze złego powodu. Najbardziej podstępny. Test sprawdzał, że użytkownik bez uprawnienia nie usunie innego użytkownika. Asercja: odpowiedź to nie sukces. Test zielony. Problem? Klient w teście w ogóle nie miał tokena. Dostawał 401, nie 403. Test przechodził bez względu na to, czy autoryzacja działa:
/* tak napisało AI: zielony, ale nic nie sprawdza */
[Fact]
public async Task UserWithoutPermission_CannotDeleteUser()
{
var client = _factory.CreateClient(); /* brak tokena! */
var response = await client.DeleteAsync($"/api/users/{OtherUserId}");
Assert.False(response.IsSuccessStatusCode); /* 401 też "nie jest sukcesem" */
}
/* po poprawce: konkretna rola i dokładnie 403 */
[Fact]
public async Task Viewer_CannotDeleteUser_Returns403()
{
var client = await CreateClientAsAsync("Viewer");
var response = await client.DeleteAsync($"/api/users/{OtherUserId}");
Assert.Equal(HttpStatusCode.Forbidden, response.StatusCode);
}Trik: zepsuj kod celowo. Jak to wyłapałem? Prostym trikiem. Celowo psujesz kod, na przykład sprawiasz, że handler autoryzacji zawsze przepuszcza. Odpalasz testy. Test dalej zielony? To znaczy, że nic nie sprawdza. Poprawka: loguję się jako Viewer i oczekuję dokładnie 403, a nie "czegokolwiek poza sukcesem". Rób to z każdym testem od AI, który przeszedł za pierwszym razem. Jeśli kiedyś zechcesz to zautomatyzować, poczytaj o testach mutacyjnych (w .NET np. Stryker.NET), ale na start w zupełności wystarczy ręczna wersja.
3 sita. Zauważ jedno: żaden z tych błędów nie wyłapał się sam. Złapały je 3 sita:
1. SPEC z przypadkami brzegowymi - złapał błąd #2.
2. Testy, które umieją się wywalić - złapały błąd #3.
3. Review diffu - złapało błąd #1.
Wyjmij jedno sito i jeden błąd przechodzi dalej.
"Gotowe, wszystko działa". A najgorsze? Po każdym kroku Claude Code pisał mniej więcej to samo:
"Gotowe, testy przechodzą, obsłużyłem wszystkie przypadki."
Z pełnym przekonaniem. Także wtedy, gdy w kodzie siedziały opisane wyżej błędy. Zielone testy to nie to samo co działający kod. Ufaj, ale czytaj diff.
Uczciwie: co da się powtórzyć, a co było szczęściem
Uczciwa matematyka. 2 dni to około 16 godzin. 3 h 10 min to około 3,2 godziny. 16 podzielić przez 3,2 daje około 5 razy szybciej. Ale to był bardzo dobry dzień. Na co dzień wychodzi mi średnio około 3 razy. Dlaczego nie 5?
Co było szczęściem. Bo część to były sprzyjające warunki:
• Znałem domenę. SPEC napisałem w 35 minut, bo wiedziałem, o co zapytać. W nowej domenie zajmie Ci to dłużej.
• Projekt był uporządkowany. Konwencje, struktura, działające testy. W legacy wygląda to zupełnie inaczej.
• Popularny temat. JWT i polityki w ASP.NET Core to bardzo popularny temat, więc AI ma tu mnóstwo przykładów, na których mogło się uczyć. Przy nietypowej logice biznesowej poprawek będzie więcej.
• 3 godziny bez spotkań i przerw. W zwykły dzień tak nie masz.
Co da się powtórzyć: przepis. Wszystko, co zależało ode mnie:
1. SPEC.md przed pierwszą linijką kodu.
2. Wskaż wzorcowy moduł zamiast opisywać konwencje.
3. Plan, akceptacja, dopiero kod. Małe kroki, commit po każdym.
4. Przypadki brzegowe spisane przed testami.
5. Każdy test od AI musi umieć się wywalić.
6. Review diffu jak u juniora.
Żaden z tych punktów nie zależy od szczęścia. Możesz zacząć od jutra.
Zmierz swój następny task. I jedna rzecz na jutro. Przy następnym tasku zapisz:
• estymację, zanim zaczniesz,
• godzinę startu i końca każdego etapu,
• na koniec sumę i porównanie z estymacją.
Bez tego za tydzień nie będziesz wiedzieć, czy AI Ci pomaga, czy tylko masz takie wrażenie. Ja bez tych notatek nie napisałbym tego artykułu. A liczby z Twojego projektu powiedzą Ci więcej niż każde demo.
Ściąga: 3 błędy AI i sita, które je złapały
Na koniec wszystkie 3 błędy w jednym miejscu. Przydadzą się przy Twoim następnym review kodu od AI:
| Błąd | Co się działo | Które sito złapało | Poprawka |
| Rola jako string: [Authorize(Roles = "Admin")] | endpoint ignoruje uprawnienia z bazy i patrzy tylko na nazwę roli | review diffu (szukanie "Roles =") | polityka ze stałej z klasy Permissions |
| Role zapisane w tokenie JWT | zwolniony admin ma pełny dostęp jeszcze do godziny, aż token wygaśnie | przypadek brzegowy w SPEC i czerwony test | token niesie tylko tożsamość, uprawnienia z bazy przez cache, zmiana ról czyści cache |
| Test zielony ze złego powodu | klient bez tokena dostaje 401, test przechodzi niezależnie od autoryzacji | celowe zepsucie kodu (handler zawsze przepuszcza) | logowanie jako Viewer i oczekiwanie dokładnie 403 |
Podsumowanie
Wszystko w jednym zdaniu: czas nie zniknął, tylko się przeniósł - z pisania kodu do myślenia i sprawdzania. I to dobra wiadomość, bo tej części AI Ci nie zabierze.
Najważniejsze rzeczy z tego taska:
• 2 dni → 3 h 10 min, ale szkielet to tylko ok. 10% czasu. Reszta to specyfikacja, przypadki brzegowe, testy i review.
• SPEC.md przed kodem: cel, zakres i poza zakresem, decyzje, konwencje, przypadki brzegowe, definition of done.
• Wzorcowy moduł zamiast eseju o konwencjach.
• Ty = tech lead, AI = junior: decyzje i review zostają po Twojej stronie.
• 3 sita: przypadki brzegowe, testy, które umieją się wywalić, i review diffu.
• Uczciwie: 5 razy szybciej to był bardzo dobry dzień, na co dzień wychodzi ok. 3 razy.
• Mierz: estymacja przed startem i czas każdego etapu.
Jeśli chcesz przejść dokładnie ten proces na swoim kodzie, robimy to w Szkole 3x Dev - szkoleniu dla programistów C# i .NET, w którym krok po kroku uczę pracy z AI. Moduł 3 nazywa się dosłownie "Pełny feature w 3 godziny zamiast 2 dni": budujesz w nim feature od encji do API. W module 4 piszesz testy, które łapią bugi, a nie tylko przechodzą, czyli dokładnie to, czego zabrakło przy błędzie numer 3.
Zapisy otwieram cyklicznie, więc wejdź na stronę i sprawdź, czy są otwarte, a jeśli nie, zostaw maila na liście oczekujących: modestprogrammer.pl/szkola-3x-dev.
A jeśli testujesz to sam: zmierz swój następny task i porównaj wynik z estymacją. Powodzenia :)