Claude Code Hooks: Kuralları Zorlamak, Formatlamak ve Testleri Otomatik Çalıştırmak
Claude Code'da bir kuralı kesin uygulamanın yolu CLAUDE.md değil hook'tur: Claude Code belli bir anda senin script'ini çalıştırır ve exit code 2 ya da deny kararı tool çağrısını durdurur. Event, matcher ve handler yapısı, stdin JSON'u, exit code'lar, PowerShell ile Windows'ta hook yazmak; Bookstore için commit'lenmiş migration koruması, değişen dosyayı formatlama, test çıktısını kısaltma ve Stop kontrolü.
Sürüm notu: Bu yazı Claude Code 2.1.325'i (13 Kasım 2026) anlatır; hook event'leri, girdi ve çıktı formatı, exit code'ları ve Windows davranışı 15 Kasım 2026'da code.claude.com/docs'taki Automate actions with hooks ve Hooks reference sayfalarından kontrol edilmiştir. Bu yazıda resmî belgelerden yararlanılmıştır. Hook script'leri PowerShell 7.7 ile örnek JSON girdileri verilerek çalıştırılmış, dotnet format adımı .NET 10 SDK ile denenmiş, settings.json yerelde JSON olarak ayrıştırılmıştır.
Önce Tahmin Et
Emre, sitedeki serilerin ortak örnek uygulaması Bookstore'da (bir online kitapçı) bir kolon tipini düzeltmesini istediğinde Claude Code'un üç hafta önce commit'lenmiş InitialCreate migration'ını düzenlediğini gördü. Oysa CLAUDE.md'de "Never edit an existing migration" satırı duruyordu ve üçüncü yazıdaki ask kuralı da her migration düzenlemesinde soru soruyordu; Emre sorulara hızlıca "evet" demişti. Ertesi gün bir PreToolUse hook'u yazdı, script hata verip exit 1 ile çıktı ve Claude aynı dosyayı yine düzenledi.
Tahminin ne? exit 1 ile biten bir hook işlemi neden durdurmadı? Ve Edit|Write için yazılmış bir hook, Claude migration dosyasını bir shell komutuyla değiştirirse bunu görür mü? Birinci cevap exit code bölümünde, ikincisi Stop hook'unda.
CLAUDE.md'nin bir talimat olduğunu, zorunlu kural olmadığını ikinci yazıda söylemiştik. Hook ise Claude Code'un belli bir anda senin komutunu çalıştırması: modelin kararına değil, script'in exit code'una bakıyor. Araçtan bağımsız ekip kuralları AI Destekli Geliştirme serisinde; burada Claude Code'daki mekanizmayı kuruyoruz.
Event, Matcher ve Handler
Bir hook üç katmandan oluşuyor. Event, hook'un hangi anda çalışacağı: PreToolUse bir tool çağrısından önce, PostToolUse başarılı bir çağrıdan sonra, Stop Claude cevabını bitirdiğinde, SessionStart session açılırken, UserPromptSubmit prompt Claude'a gitmeden önce, Notification Claude Code senden bir şey beklediğinde. Listede SubagentStop, PreCompact, WorktreeCreate, InstructionsLoaded gibi otuzdan fazla event var. Matcher, event içinde filtre: tool event'lerinde tool adına bakıyor (Edit|Write, Bash|PowerShell, mcp__.*). Handler da çalışacak şey: çoğunlukla bir komut, ama bir HTTP endpoint'i, bir MCP (Model Context Protocol; dış araç ve veri kaynaklarına bağlanma protokolü) tool'u ya da bir modele soru soran prompt handler'ı da olabiliyor.
Hook'lar ayar dosyalarında hooks anahtarının altında duruyor: kişisel ~/.claude/settings.json, ekibin commit'lediği .claude/settings.json, kişisel .claude/settings.local.json, managed settings, plugin'ler ve skill ya da subagent frontmatter'ı. Seviyeler birbirini ezmiyor, birleşiyor. Aynı event'e uyan hook'lar paralel çalışıyor; birinin deny dönmesi diğerlerini durdurmuyor.
Girdi, Exit Code ve JSON Çıktı
Claude Code, event'in verisini JSON olarak script'in stdin'ine veriyor. Ortak alanlar session_id, cwd, hook_event_name; tool event'lerinde bunlara tool_name ve tool_input ekleniyor. Edit, Write ve Read için tool_input.file_path her zaman mutlak yol ve Windows'ta ters bölü işaretiyle geliyor.
Script'in cevabı exit code'u. 0 "itirazım yok" demek; PreToolUse'ta bu bir onay değil, çağrı normal permission akışına devam ediyor. 2 engelliyor ve stderr'e yazdığın metin Claude'a gerekçe olarak gidiyor. Diğer her exit code engellemeyen bir hata: transcript'te bir uyarı çıkıyor ve işlem devam ediyor. Tahmindeki ilk sorunun cevabı bu: Unix'te alışkın olduğumuz exit 1 burada politika uygulamıyor. Belgeler de bir kuralı zorlayan hook için exit 2'yi öneriyor.
Daha fazla kontrol için exit 0 ile tek bir JSON object yazdırıyorsun. PreToolUse'ta hookSpecificOutput.permissionDecision dört değer alıyor: deny çağrıyı iptal edip gerekçeyi Claude'a veriyor, ask sana soruyor, allow soruyu atlıyor ve defer yalnız claude -p'de çağrıyı sonraya bırakıyor. allow dönsen bile deny ve ask kuralları yine uygulanıyor; birden fazla hook farklı karar verirse en kısıtlayıcı olan kazanıyor.
Bookstore: Commit'lenmiş Migration'ları Korumak
Üçüncü yazıda kesin bir engel için permission kuralını önermiştik, ama kuralın göremediği bir ayrım var: /add-migration ile az önce oluşturulan, henüz commit'lenmemiş bir migration'ı düzeltmek normal; commit'lenmiş, belki production veritabanına uygulanmış bir migration'ı değiştirmek değil. Path pattern'i bu farkı bilemez, git bilir. .claude/hooks/protect-migrations.ps1:
# PreToolUse (Edit|Write): commit'lenmiş migration dosyalarını korur; yeniler düzenlenebilir.
$payload = [Console]::In.ReadToEnd() | ConvertFrom-Json
$path = [string]$payload.tool_input.file_path
if (-not $path) { exit 0 }
# Windows ters bölü gönderir; ileri bölüyle karşılaştır.
$normalized = $path.Replace('\', '/')
if ($normalized -notmatch '/src/Bookstore\.Api/Data/Migrations/') { exit 0 }
if (-not (Test-Path -LiteralPath $path)) { exit 0 }
$name = Split-Path -Leaf $path
git -C (Split-Path -Parent $path) ls-files --error-unmatch -- $name *> $null
if ($LASTEXITCODE -ne 0) { exit 0 }
[ordered]@{
hookSpecificOutput = [ordered]@{
hookEventName = 'PreToolUse'
permissionDecision = 'deny'
permissionDecisionReason = "$name is a committed migration file. Never edit it; add a new migration with /add-migration."
}
} | ConvertTo-Json -Depth 5 -Compress
exit 0
Dosya henüz yoksa (Write ile yeni migration) ya da git onu tanımıyorsa script sessizce çıkıyor. Git'in bildiği bir dosyaysa deny dönüyor; gerekçe Claude'a gidiyor ve Claude yeni bir migration oluşturmaya yöneliyor. BookstoreContextModelSnapshot.cs da commit'li olduğu için korunuyor; onu yalnız dotnet ef güncellemeli. Hook'u Claude Code olmadan, örnek bir girdiyle repo kökünden deneyebilirsin:
$payload = @{
tool_name = 'Edit'
tool_input = @{ file_path = (Resolve-Path "src/Bookstore.Api/Data/Migrations/$file").Path }
} | ConvertTo-Json -Compress
$payload | pwsh -NoProfile -File .claude/hooks/protect-migrations.ps1
$file commit'li 20261101120000_InitialCreate.cs iken çıktı aşağıdaki gibi; yeni ve commit'lenmemiş 20261115090000_AddLowStockAlerts.cs için çıktı boş, exit code ikisinde de 0:
{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":"20261101120000_InitialCreate.cs is a committed migration file. Never edit it; add a new migration with /add-migration."}}
Hook bu ayrımı yaptığı için üçüncü yazıdaki Edit(/src/Bookstore.Api/Data/Migrations/**) ask kuralını settings.json'dan kaldırdık. appsettings.Production.json için ayrı bir hook'a gerek yok: Configure permissions sayfasına göre bir Read deny kuralı aynı yoldaki Edit ve Write çağrılarını da engelliyor ve üçüncü yazıdaki Read(**/appsettings.Production.json) kuralı zaten yerinde. Yalnız yola bakan bir yasak için doğru araç permission kuralı; hook'u, kuralın ifade edemediği mantık için sakla.
Bookstore: Değişen Dosyayı Formatlamak
PostToolUse hook'u tool çalıştıktan sonra devreye giriyor, yani hiçbir şeyi engelleyemiyor ama arkasını toplayabiliyor. .claude/hooks/format-file.ps1 yalnız Claude'un değiştirdiği dosyayı formatlıyor:
# PostToolUse (Edit|Write): yalnız Claude'un değiştirdiği dosyayı formatlar.
$payload = [Console]::In.ReadToEnd() | ConvertFrom-Json
$path = [string]$payload.tool_input.file_path
if (-not $path -or -not (Test-Path -LiteralPath $path)) { exit 0 }
$root = git -C (Split-Path -Parent $path) rev-parse --show-toplevel 2>$null
if ($LASTEXITCODE -ne 0) { exit 0 }
Push-Location $root
try {
$relative = (Resolve-Path -LiteralPath $path -Relative) -replace '^\.[\\/]', ''
if ($relative -match '\.cs$') {
$output = dotnet format whitespace . --folder --include $relative 2>&1
}
elseif ($relative -match '^web[\\/]bookstore-web[\\/].*\.(ts|html|scss)$' -and
(Test-Path -LiteralPath 'web/bookstore-web/node_modules/prettier')) {
Push-Location 'web/bookstore-web'
$output = npx prettier --write $path 2>&1
Pop-Location
}
else {
exit 0
}
if ($LASTEXITCODE -ne 0) {
[Console]::Error.WriteLine("format failed for ${relative}: $output")
exit 1
}
}
finally {
Pop-Location
}
exit 0
dotnet format whitespace --folder projeyi yüklemeden, yalnız .editorconfig'teki boşluk kurallarını uyguluyor; her edit'ten sonra çalışacak bir komut için hızlı olması önemli. Tam style ve analyzer kontrolünü CI'daki dotnet format --verify-no-changes'a bırakıyoruz. Angular tarafında Prettier yalnız bookstore-web'in node_modules'unda kuruluysa çalışıyor; script paketi kendisi kurmaya çalışmıyor. Biçimi bozuk bir BookEndpoints.cs için örnek bir Edit girdisi verince hook dosyayı şöyle değiştirdi:
diff --git a/src/Bookstore.Api/Catalog/BookEndpoints.cs b/src/Bookstore.Api/Catalog/BookEndpoints.cs
index f9f03ab..1cf1036 100644
--- a/src/Bookstore.Api/Catalog/BookEndpoints.cs
+++ b/src/Bookstore.Api/Catalog/BookEndpoints.cs
@@ -2,5 +2,5 @@ namespace Bookstore.Api.Catalog;
public static class BookEndpoints
{
-public static string Title(string isbn) => isbn;
+ public static string Title(string isbn) => isbn;
}
Formatlama başarısız olursa script exit 1 ile çıkıyor: bu bilerek engellemeyen bir hata, çünkü edit zaten yapıldı ve formatlanamaması işi durdurmamalı.
Bookstore: Test Çıktısını Kısaltmak ve Stop'ta Kontrol
PostToolUse, tool'un Claude'a giden çıktısını updatedToolOutput ile değiştirebiliyor. Başarılı bir dotnet test çıktısının çoğu build satırı; Claude'un ihtiyacı uyarılar ve özet. .claude/hooks/short-test-output.ps1:
# PostToolUse (exit 0 ile biten dotnet test): uyarıları, hataları ve son satırları tutar.
$payload = [Console]::In.ReadToEnd() | ConvertFrom-Json
$response = $payload.tool_response
if (-not $response -or -not ($response.PSObject.Properties.Name -contains 'stdout')) { exit 0 }
$lines = @(([string]$response.stdout) -split "\r?\n")
if ($lines.Count -le 60) { exit 0 }
$important = @($lines | Where-Object { $_ -match '\b(error|warning|failed)\b' } | Select-Object -First 40)
$tail = @($lines | Select-Object -Last 15)
$kept = $important + $tail
$header = "[output shortened by .claude/hooks/short-test-output.ps1: kept $($kept.Count) of $($lines.Count) lines]"
$response.stdout = (@($header) + $kept) -join "`n"
[ordered]@{
hookSpecificOutput = [ordered]@{
hookEventName = 'PostToolUse'
updatedToolOutput = $response
}
} | ConvertTo-Json -Depth 10 -Compress
exit 0
Belgelerdeki bir ayrım bu hook'un sınırını çiziyor. PostToolUse yalnız başarılı tool çağrısından sonra çalışıyor; belgelere göre sıfır dışı bir exit code'la biten komut PostToolUseFailure event'ini tetikliyor ve o event çıktıyı değiştiremiyor, yalnız additionalContext ekleyebiliyor. Yani kırmızı testlerin uzun çıktısını bu hook kısaltmıyor; o iş altıncı yazıdaki test-runner subagent'ının. Script, tool_response'ta stdout alanı yoksa da hiçbir şey yapmıyor, çünkü belgeler çıktı yapısını Bash için veriyor ve bu yapıyı tahmin etmek istemiyoruz. Değiştirilen çıktının bir başlık satırı taşıması da bilinçli: Claude neyin kesildiğini bilmeli.
Tahmindeki ikinci sorunun cevabı burada. Edit|Write matcher'ı, Claude dosyayı Set-Content ya da sed gibi bir shell komutuyla değiştirirse çalışmıyor. Belgelerin önerisi, her turun sonunda çalışma ağacını tarayan bir Stop hook'u. .claude/hooks/stop-guard.ps1 iki şeye bakıyor: commit'lenmiş bir migration değişmiş mi ve C# dosyaları değiştiyse testler yeşil mi:
# Stop: commit'lenmiş migration değiştiyse ya da API testleri kırmızıysa Claude'u çalışmaya devam ettirir.
$payload = [Console]::In.ReadToEnd() | ConvertFrom-Json
$root = git -C $payload.cwd rev-parse --show-toplevel 2>$null
if ($LASTEXITCODE -ne 0) { exit 0 }
function Block([string]$reason) {
if ($payload.stop_hook_active) {
# Bu hook yüzünden bir kez devam edildi: kullanıcıya söyle, Claude dursun.
[ordered]@{ systemMessage = "stop-guard: $reason" } | ConvertTo-Json -Compress
}
else {
[ordered]@{ decision = 'block'; reason = $reason } | ConvertTo-Json -Compress
}
exit 0
}
# 1. Herhangi bir tool'un, shell komutları dahil, değiştirdiği commit'li migration'lar.
$migrations = @(git -C $root diff --name-only --diff-filter=MD HEAD -- 'src/Bookstore.Api/Data/Migrations' |
Where-Object { $_ -notmatch 'ModelSnapshot\.cs$' })
if ($migrations.Count -gt 0) {
Block ("Committed migrations were changed: " + ($migrations -join ', ') +
". Restore them with git restore and add a new migration instead.")
}
# 2. API testleri: yalnız son yeşil koşudan beri C# dosyaları değiştiyse.
$changes = git -C $root status --porcelain -- '*.cs' '*.csproj'
if (-not $changes) { exit 0 }
$diff = (git -C $root diff HEAD -- '*.cs' '*.csproj') + ($changes -join "`n")
$sha = [System.Security.Cryptography.SHA256]::Create()
$hash = [BitConverter]::ToString($sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($diff))) -replace '-', ''
$stamp = Join-Path ([IO.Path]::GetTempPath()) "bookstore-green-$($payload.session_id).txt"
if ((Test-Path -LiteralPath $stamp) -and ((Get-Content -LiteralPath $stamp -Raw).Trim() -eq $hash)) { exit 0 }
$output = dotnet test (Join-Path $root 'Bookstore.slnx') --nologo 2>&1
if ($LASTEXITCODE -eq 0) {
Set-Content -LiteralPath $stamp -Value $hash
exit 0
}
$failures = @($output | Where-Object { "$_" -match '\b(error|failed)\b' } | Select-Object -First 15)
Block ("dotnet test failed (exit $LASTEXITCODE). Fix these before finishing:`n" + ($failures -join "`n"))
Stop'ta decision: "block" Claude'un durmasını engelliyor ve reason onun bir sonraki talimatı oluyor. Sonsuz döngüye karşı iki koruma var: girdi JSON'undaki stop_hook_active, Claude zaten bir Stop hook'u yüzünden devam ediyorsa true geliyor ve script bu durumda engellemek yerine sana bir systemMessage gösteriyor. Ayrıca Claude Code, Stop hook'ları turu art arda sekiz kez uzattıktan sonra dokuzuncu engeli yok sayıyor. Değişiklik özetini bir hash olarak saklamak da testlerin her cevaptan sonra değil, yalnız C# kodu değiştiğinde koşmasını sağlıyor.
settings.json'a Bağlamak ve Windows Notları
Dört script'i ekibin commit'lediği .claude/settings.json'da, permission bloğunun yanındaki tek bir hooks anahtarına bağlıyoruz:
{
"hooks": {
"PreToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"shell": "powershell",
"command": "& \"$env:CLAUDE_PROJECT_DIR\\.claude\\hooks\\protect-migrations.ps1\"",
"onFailure": "block",
"timeout": 30
}
]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"shell": "powershell",
"command": "& \"$env:CLAUDE_PROJECT_DIR\\.claude\\hooks\\format-file.ps1\"",
"timeout": 60,
"statusMessage": "Formatting the changed file"
}
]
},
{
"matcher": "Bash|PowerShell",
"hooks": [
{
"type": "command",
"if": "Bash(dotnet test *)",
"shell": "powershell",
"command": "& \"$env:CLAUDE_PROJECT_DIR\\.claude\\hooks\\short-test-output.ps1\""
},
{
"type": "command",
"if": "PowerShell(dotnet test *)",
"shell": "powershell",
"command": "& \"$env:CLAUDE_PROJECT_DIR\\.claude\\hooks\\short-test-output.ps1\""
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "command",
"shell": "powershell",
"command": "& \"$env:CLAUDE_PROJECT_DIR\\.claude\\hooks\\stop-guard.ps1\"",
"timeout": 600,
"statusMessage": "Checking migrations and API tests"
}
]
}
]
}
}
"shell": "powershell" hook'u Windows'ta PowerShell'de çalıştırıyor; Claude Code önce pwsh.exe'yi arıyor, yoksa Windows PowerShell 5.1'e düşüyor. Script'leri bu yüzden 5.1'de de çalışacak biçimde yazdık ve jq yerine ConvertFrom-Json kullandık. Proje kökü için $env:CLAUDE_PROJECT_DIR yazılmalı; çıplak $CLAUDE_PROJECT_DIR PowerShell'de tanımsız bir değişken olarak boşa çıkıyor. if alanı permission kuralı syntax'ı ile handler'ı daraltıyor ama tek bir kural alıyor; Bash ve PowerShell için iki ayrı handler bu yüzden. Matcher'ı Bash|PowerShell yazmak şart, çünkü Git Bash'i olmayan bir Windows makinesinde Bash tool'u hiç yok.
Koruma hook'undaki "onFailure": "block" önemli: belgelere göre hook başlatılamazsa, zaman aşımına uğrarsa ya da 0 ve 2 dışında bir kodla çıkarsa çoğu event'te işlem devam ediyor. Script yolunu yanlış yazdığın bir koruma her şeyi geçirir; block bu durumda çağrıyı durduruyor. Varsayılan zaman aşımı komut hook'larında 600 saniye; Stop hook'una bu süreyi bilerek verdik, diğerlerini kısalttık.
Doğrulamak ve Hata Ayıklamak
/hooks salt okunur bir tarayıcı açıyor: her hook'un hangi dosyadan geldiğini ve tam olarak ne çalıştırdığını gösteriyor. Ayrıntı için claude --debug ile başlatıp debug log'una bakıyorsun; v2.1.296'dan beri tool event'lerindeki ve Stop gibi event'lerdeki komut hook'ları log'a süresiyle bir satır bırakıyor, yavaş hook'u buradan buluyorsun. Bütün hook'ları geçici olarak kapatmak için "disableAllHooks": true var, ama tek bir hook'u konfigürasyonda tutup kapatmanın yolu yok.
İkinci yazıda path-scoped kuralların ne zaman yüklendiğini görmek için bir hook sözü vermiştik. InstructionsLoaded event'i bir CLAUDE.md ya da .claude/rules/*.md dosyası context'e girdiğinde çalışıyor ve hiçbir şeyi engelleyemiyor; yalnız gözlem için. Kişisel .claude/settings.local.json'a:
{
"hooks": {
"InstructionsLoaded": [
{
"hooks": [
{
"type": "command",
"shell": "powershell",
"command": "$p = [Console]::In.ReadToEnd() | ConvertFrom-Json; Add-Content -LiteralPath \"$env:TEMP\\bookstore-instructions.log\" -Value \"$($p.load_reason) $($p.memory_type) $($p.file_path)\""
}
]
}
]
}
}
Log'daki load_reason sana dosyanın açılışta mı (session_start), bir path pattern'i eşleştiği için mi (path_glob_match) yoksa bir import yüzünden mi (include) yüklendiğini söylüyor.
Üretimde Dikkat
Komut hook'ları senin kullanıcı yetkinle çalışıyor ve erişebildiğin her dosyayı okuyup silebiliyor. Repo'dan gelen hook'ları kod gibi review et. Belgelere göre interaktif session'da hook'lar klasör için workspace trust onayı verilene kadar bekletiliyor, ama claude -p ve Agent SDK koşularında bu dialog yok ve klasör güvenilir sayılıyor: repo'nun .claude/settings.json'ındaki hook'lar hiç güvenmediğin bir klasörde de çalışıyor. Yazmadığın bir repo'da claude -p çalıştırmadan önce .claude/ klasörünü oku ya da o koşu için --settings '{"disableAllHooks": true}' ver.
Hook'lar subagent'ların içinde de çalışıyor; altıncı yazıdaki subagent'ların edit'leri de migration korumasından geçiyor. Organizasyon tarafında yönetici allowManagedHooksOnly ile yalnız managed settings'teki hook'lara izin verebiliyor; bu ayarlar on birinci yazıda.
Hook'u hızlı tut. PreToolUse her Edit ve Write'tan önce çalışıyor; yavaş bir script her adımı yavaşlatıyor. Ağır kontrolleri Stop'a ya da CI'a taşı.
Sık Düşülen Tuzaklar
"CLAUDE.md'ye yazdım ya." Talimat context'tir; zorunlu kural permission ya da hook'tur.
Politika hook'unda exit 1. Engellemiyor; exit 2 ya da permissionDecision: "deny" kullan ve onFailure: "block" ekle.
Yalnız Edit|Write'ı izlemek. Shell komutlarıyla yapılan değişiklikleri görmek için Stop'ta çalışma ağacını tara.
Windows yolunu ileri bölüyle karşılaştırmak. file_path ters bölüyle geliyor; normalize etmeden yaptığın karşılaştırma hiç eşleşmez ve hook sessizce her şeyi geçirir.
Yanlış seviyedeki JSON alanı. PreToolUse kararı hookSpecificOutput içinde, Stop ve PostToolUse'un decision'ı en üst seviyede; yanlış yere yazılan alan etkisiz kalıyor.
Küçük Bir Deneme
★☆☆ protect-migrations.ps1'i kendi projendeki bir klasöre uyarla ve yukarıdaki PowerShell parçasıyla commit'li ve yeni bir dosya için çalıştır. İki çıktıyı karşılaştır.
★★☆ Bir Notification hook'u yaz: Claude onay beklediğinde Windows'ta bir bildirim göstersin. /hooks ile göründüğünü doğrula ve Manual mode'da onay gerektiren bir işlem iste.
★★★ stop-guard.ps1'e Angular tarafını ekle: web/bookstore-web altında .ts dosyaları değiştiyse npm test çalışsın. Hash'i iki taraf için ayrı tut ve bir turda gereksiz koşu olup olmadığını debug log'undaki süre satırlarından ölç.
Hook'larla Claude Code'un kendi tool'larını denetledik. Sıradaki adım ona yeni tool'lar vermek: AI Agent'ları ve MCP serisinin dokuzuncu yazısındaki C# MCP server'ını, salt okunur bir SQL Server bağlantısını ve Angular arayüzünü test etmek için bir tarayıcıyı Claude Code'a bağlamak. Sekizinci yazıda claude mcp add, scope'lar ve .mcp.json var.