TEKNİK YAZI / Developer Tools ve Açık Kaynak

Kodlama Ajanınızı Kim Yapılandırıyor? Postman Olayı ve Hook Güven Sınırı

Postman masaüstü uygulaması Claude Code ve Codex'e sormadan eklenti kurdu. Hook onayı neden güvenlik sınırı değil, ajan ayarlarınızı nasıl denetlersiniz?

Kodlama ajanı yapılandırması, ajan oturumu başlarken yüklenen hook, eklenti, MCP sunucusu ve talimat dosyalarının toplamıdır. Bu dosyalar bir sonraki oturumda hangi komutun çalışacağını ve modelin neyi okuyacağını belirler. Ekim sonunda ortaya çıkan Postman olayı, bu ayarların artık yalnızca geliştiricinin elinde olmadığını gösterdi: Masaüstü uygulaması, mevcut kullanıcıların makinesinde Claude Code ve Codex’e eklenti kurdu.

Olay kötü amaçlı bir saldırı değil, rıza ve güven sınırı tartışması. Ama aynı mekanizma birkaç gün sonra gerçekten kötü amaçlı bir eklenti tarafından da kullanıldı. Bu yazı neler olduğunu kanıtın elverdiği ölçüde anlatıyor, hook onay kaydının neyi koruyup neyi korumadığını deneyle gösteriyor ve yapılandırma değişikliklerini yakalayan küçük bir araç sunuyor. Bilgiler 6 Kasım 2026 akşamının durumunu yansıtıyor. Postman 5 Kasım’da değişikliği geri aldığını duyurdu ve kaydı kapattı.

Kısaca

Postman olayında ne oldu?

GitHub’daki kayda göre olay şöyle ilerledi:

  1. 24 Ekim: Bir Mac’te Postman 12.33.4 açıldı. Uygulama günlüğü, auto-existing-upgrade kaynağıyla claude-code:installed ve codex:installed sonuçlarını kaydetti.
  2. Codex tarafı: ~/.codex/config.toml içine Postman eklenti pazarı, etkin eklenti ve bir hooks.state kaydı girdi. MCP yapılandırması da mcp.postman.com adresine bağlandı ve kimlik doğrulama hatası vermeye başladı.
  3. Claude Code tarafı: ~/.claude/settings.json içinde postman@claude-plugins-official etkinleşti, eklenti kullanıcı kapsamında kuruldu.
  4. CLI: ~/.local/bin içine Postman CLI kuruldu.
  5. 3 Kasım: Başka bir kullanıcı, 12.31.3’ten 12.35.1’e güncellemede aynı akışı bildirdi. Bu makinede trusted_hash kaydı yoktu.
  6. Copilot: Bir başka kullanıcı ~/.copilot/settings.json içinde Postman eklentisini buldu. Uygulamadaki kaldırma seçeneği onu kaldırmamış.
  7. 5 Kasım: postmanlabs organizasyonunun bir üyesi, deneyimi doğru kontrol düzeyiyle yeniden tasarlayacaklarını ve o zamana kadar değişikliği geri aldıklarını yazdı. Kayıt aynı gün “tamamlandı” olarak kapatılıp tartışmaya kilitlendi.

Eklentinin oturum başlangıcı hook’u, modele API işlerinde Postman becerisini yüklemesini söyleyen bir metin yazdırıyor. Şikâyetin somut karşılığı da bu: Geliştirici, API işlerinin neden birden Postman üzerinden yönlendirildiğini araştırırken bu kurulumu bulmuş. Aynı metin, kullanıcının kendi talimatlarının öncelikli olduğunu da söylüyor.

Neyi biliyoruz, neyi bilmiyoruz?

Bu olayda haber başlıkları kanıtın önüne geçti; bu yüzden bilinenle bilinmeyeni ayırmak gerekiyor:

Hook ve eklenti neden bu kadar güçlü?

Bir hook, ajanın yaşam döngüsünde otomatik çalışan bir komuttur. Oturum başlangıcı hook’unun iki etkisi vardır:

  1. Komut çalışır. Claude Code belgesine göre komut hook’ları Claude Code’un ortamını devralır. Bu nedenle hook’lar otomatik çalışan kabuk komutları gibi değerlendirilmelidir.
  2. Çıktı modele girer. Hem Claude Code hem Codex belgesine göre oturum başlangıcı hook’unun standart çıktısı modelin bağlamına eklenir.

Bir eklenti ise hook, MCP sunucusu ve beceriyi tek pakette getirebilir. Tek bir kurulum, ajanın davranışını birkaç katmanda birden değiştirebilir.

Masaüstü uygulaması, eklenti pazarı, klonlanan depo ve paket bağımlılığı; hook, eklenti, MCP sunucusu, talimat dosyası ve onay kaydına yazabiliyor. Oturumda bunlar kabuk komutu çalıştırıyor, modelin bağlamına metin ekliyor ve yeni araçlar açıyor
Şekil 1. Ajan yapılandırması farklı yerlerden yazılabilir ve bir sonraki oturumu doğrudan etkiler.

Bu ayarlar araçlarda benzer yerlerde durur:

AyarClaude CodeCodexCursor
Kullanıcı hook’u~/.claude/settings.json~/.codex/hooks.json, ~/.codex/config.toml~/.cursor/hooks.json
Proje hook’u.claude/settings.json.codex/hooks.json, .codex/config.toml.cursor/hooks.json
Eklenti kaydı~/.claude/plugins/installed_plugins.json~/.codex/config.toml~/.cursor/extensions
MCP~/.claude.json~/.codex/config.toml~/.cursor/mcp.json
TalimatCLAUDE.md, becerilerAGENTS.md, beceriler.cursor/rules

Onay davranışı ise aynı değil. Codex belgesine göre yeni ya da değişmiş hook, eklentiyle gelse bile incelenip onaylanana kadar atlanır. Claude Code’da proje ayarları klasör güvenine bağlı, ama kullanıcı ayarları için ayrı bir onay kaydı yok; belgeye göre eklenti etkinleştirildiğinde hook’ları da devreye girer. Bunu bir tasarım tercihi olarak görüyorum: Kullanıcı ayarlarına yazabilen zaten kullanıcının yetkisine sahiptir.

Deney: Onay kaydı neyi durduruyor?

Postman kurulum programının yaptığı çağrıları, zararsız bir hook ile kendi ortamımda tekrarladım. Düzenek şöyleydi:

AdımSonuç
hooks/listHook durumu untrusted
config/batchWriteconfig.toml içine trusted_hash girdi
hooks/list yenidenHook durumu trusted
Hook çalıştı mı?Hayır, oturum açılmadı

Bu iki çağrı, Postman’in açık kaynak kurulum programındaki çağrılarla aynı. Arada insan onayı yok. Foma’nın bağımsız deneme notu da Codex 0.162.1’de benzer sonucu, üstelik onaylanan hook’un çalıştığını bildiriyor.

Deneyin sınırlarını açık söyleyeyim. Sınadığım şey kullanıcı düzeyi bir hook, eklenti hook’u değil. Codex belgesi eklenti paketi değişince yeniden inceleme istendiğini söylüyor. Hook’u çalıştırmadım, çünkü bu bir model çağrısı gerektiriyordu.

Bu sonuç bir Codex açığı değil. Onay kaydı işe yarar bir şey yapıyor: Depodan gelen ya da sonradan değişen bir hook’un fark edilmeden çalışmasını engelliyor. Ama aynı kullanıcı hesabıyla çalışan bir programa karşı sınır değil, çünkü işletim sistemi açısından o program zaten sizsiniz. Bir onay mekanizmasının gücü, kaydını kimin yazabildiğiyle sınırlıdır.

Solda kullanıcı, kurulum programı ve herhangi bir süreç aynı config.toml dosyasına yazabiliyor; denemede hook untrusted durumundan trusted durumuna geçiyor. Sağda yöneticinin dağıttığı, root sahipli ve merkezden yönetilen ayarlara normal kullanıcı süreci yazamıyor
Şekil 2. Aynı kullanıcı hesabında onay kaydı bir uyarı işlevi görür. Sınır, yazma yetkisinin ayrıldığı yerde başlar.

Aynı mekanizma, üç farklı olay

Bu olayları aynı kefeye koymak yanlış olur. Niyet ve zarar farklı, kullanılan mekanizma aynı:

OlayTarihNe oldu?Niteliği
Postman eklentileriEkim-Kasım 2026Meşru uygulama, başka ajanlara eklenti ve CLI kurduRıza sorunu, zarar kanıtı yok
Cursor’daki eklenti29 Ekim 2026Claude Code eklentisinin 2.1.312 ve 2.1.313 sürümleri yaklaşık 11 saat SSH anahtarı ve ters tünel kurmaya çalıştıKötü amaçlı
NVIDIA AGENTS.md denemesiNisan 2026Zararlı bağımlılık derleme sırasında talimat dosyası yazdı, ajan onu izlediGüvenlik araştırması

Cursor bildirimine göre etkilenen sürümler yalnız Windows ve macOS içindi ve eklentiyi kaldırmak tek başına yeterli olmayabilir. Bu eklentiyi kullandıysanız bildirimi okuyup authorized_keys dosyanızda svc-telemetry anahtarını arayın. Cursor, paketin pazara nasıl girdiğini henüz açıklamadı.

Yapılandırmanızı nasıl denetlersiniz?

Önce araçların kendi gösterdiklerine bakın. Claude Code ve Codex’te /hooks komutu yüklü hook’ları kaynağıyla gösterir. Postman kullanıyorsanız kurulumun günlükte izi var:

grep -E 'auto-existing-upgrade|autoInstall\.(plugin|cli)' \
  "$HOME/Library/Application Support/Postman/logs/main.log" | tail -5

Uygulamadaki kaldırma seçeneği Help menüsündeki Postman CLI and Plugins altında. Kaldırdıktan sonra ajan yapılandırmalarını ayrıca kontrol edin, çünkü bir kullanıcı Copilot kaydının kaldığını bildirdi. Geri alma duyurusu, daha önce kurulan bileşenlerin kendiliğinden kaldırılacağını söylemiyor.

Bunlar anlık bakış sağlar. Asıl ihtiyaç, değişikliği ilk fırsatta fark etmek. Bunun için yapılandırmanın anlık kopyasını alan küçük bir betik yazdım:

python3 ajan_yuzey.py --write ~/.ajan-taban.json
python3 ajan_yuzey.py --baseline ~/.ajan-taban.json

İlk komut anlık kopyayı kaydeder, ikincisi farkı gösterir ve fark varsa çıkış kodu 1 döner. Sahte bir ev dizininde Postman kaydındaki düzeni taklit ettiğimde şu çıktıyı verdi:

EKLENDI  claude eklenti postman@claude-plugins-official user
EKLENDI  claude eklenti-acik postman@claude-plugins-official
EKLENDI  claude eklenti-icerik postman@claude-plugins-official
EKLENDI  cli postman
EKLENDI  codex eklenti postman@postman
EKLENDI  codex hook-onayi postman@postman:hooks/codex-hooks.json:session_start:0:0
EKLENDI  codex pazar postman
EKLENDI  copilot eklenti-acik postman@postman
EKLENDI  copilot pazar postman
ajan_yuzey.py dosyasının tamamı
#!/usr/bin/env python3
"""Kodlama ajanlarının başlangıçta yüklediği ayarların anlık kopyasını alır ve karşılaştırır.

  ajan_yuzey.py --write taban.json      temel kopyayı kaydeder
  ajan_yuzey.py --baseline taban.json   farkı gösterir; fark varsa çıkış kodu 1

Python 3.11 ve üstü gerekir. Sır yazmaz: env ve başlık değerleri okunmaz, komut ve argümanlar yalnız özetlenir.
"""
import argparse, hashlib, json, os, shlex, sys, tomllib
from pathlib import Path


def digest(value):
    return hashlib.sha256(json.dumps(value, sort_keys=True, default=str).encode()).hexdigest()[:16]


def file_digest(path):
    return hashlib.sha256(path.read_bytes()).hexdigest()[:16]


def tree_digest(root):
    files = sorted(p for p in root.rglob("*") if p.is_file() and ".git" not in p.parts)
    return digest([[str(p.relative_to(root)), file_digest(p)] for p in files])


def load(path, items, agent):
    if not path.is_file():
        return {}
    try:
        return tomllib.loads(path.read_text()) if path.suffix == ".toml" else json.loads(path.read_text())
    except (ValueError, OSError):
        items[f"{agent} okunamadi {path.name}"] = "hata"
        return {}


def add_hooks(hooks, agent, items):
    """Komut metnini yazmaz; komutun çağırdığı dosyanın içeriğini de özete katar."""
    for event, groups in (hooks or {}).items():
        for group in groups if isinstance(groups, list) else [groups]:
            for handler in group.get("hooks", [group]) if isinstance(group, dict) else []:
                command = str(handler.get("command", ""))
                expanded = os.path.expandvars(command)
                try:
                    words = shlex.split(expanded)
                except ValueError:
                    words = expanded.split()
                targets = [Path(w).expanduser() for w in words if "/" in w]
                content = [file_digest(t) for t in targets if t.is_file()]
                label = Path(words[0]).name if words else "?"
                items[f"{agent} hook {event} {label} {digest(command)[:8]}"] = digest([group.get("matcher"), content])


def add_mcp(servers, agent, items):
    """Yalnız sunucu adı görünür; env ve başlıkların değeri özete de girmez."""
    for name, config in (servers or {}).items():
        safe = {k: v for k, v in config.items() if k not in ("env", "headers", "http_headers")}
        safe["env_adlari"] = sorted(config.get("env", {}))
        items[f"{agent} mcp {name}"] = digest(safe)


def add_files(root, patterns, agent, items):
    for pattern in patterns:
        for path in sorted(root.glob(pattern)):
            if path.is_file():
                items[f"{agent} talimat {path.relative_to(root)}"] = file_digest(path)


def snapshot(home):
    items = {}

    claude = home / ".claude"
    settings = load(claude / "settings.json", items, "claude")
    add_hooks(settings.get("hooks"), "claude", items)
    for name, enabled in settings.get("enabledPlugins", {}).items():
        items[f"claude eklenti-acik {name}"] = str(enabled)
    for name, source in settings.get("extraKnownMarketplaces", {}).items():
        items[f"claude pazar {name}"] = digest(source)
    installed = load(claude / "plugins/installed_plugins.json", items, "claude")
    for name, entries in installed.get("plugins", {}).items():
        for entry in entries:
            items[f"claude eklenti {name} {entry.get('scope', '?')}"] = entry.get("version", "?")
            install_dir = Path(entry.get("installPath", ""))
            if install_dir.is_dir():
                items[f"claude eklenti-icerik {name}"] = tree_digest(install_dir)
    for name in load(claude / "plugins/known_marketplaces.json", items, "claude"):
        items[f"claude bilinen-pazar {name}"] = "var"
    state = load(home / ".claude.json", items, "claude")
    add_mcp(state.get("mcpServers"), "claude", items)
    add_files(claude, ["CLAUDE.md", "skills/*/SKILL.md", "agents/*.md", "commands/*.md"], "claude", items)

    codex = home / ".codex"
    config = load(codex / "config.toml", items, "codex")
    for name, value in config.get("marketplaces", {}).items():
        items[f"codex pazar {name}"] = digest(value)
    for name, value in config.get("plugins", {}).items():
        items[f"codex eklenti {name}"] = digest(value)
    for name, value in config.get("hooks", {}).get("state", {}).items():
        items[f"codex hook-onayi {name}"] = digest(value)
    add_mcp(config.get("mcp_servers"), "codex", items)
    add_hooks(load(codex / "hooks.json", items, "codex").get("hooks"), "codex", items)
    add_files(codex, ["AGENTS.md", "skills/*/SKILL.md"], "codex", items)

    cursor = home / ".cursor"
    add_mcp(load(cursor / "mcp.json", items, "cursor").get("mcpServers"), "cursor", items)
    add_hooks(load(cursor / "hooks.json", items, "cursor").get("hooks"), "cursor", items)
    add_files(cursor, ["rules/*"], "cursor", items)
    if (cursor / "extensions").is_dir():
        for path in sorted((cursor / "extensions").iterdir()):
            if path.is_dir():
                items[f"cursor eklenti {path.name}"] = "var"

    copilot = load(home / ".copilot/settings.json", items, "copilot")
    for name, enabled in copilot.get("enabledPlugins", {}).items():
        items[f"copilot eklenti-acik {name}"] = str(enabled)
    for name, source in copilot.get("extraKnownMarketplaces", {}).items():
        items[f"copilot pazar {name}"] = digest(source)

    add_files(home / ".agents", ["skills/*/SKILL.md"], "ortak", items)
    bin_dir = home / ".local/bin"
    if bin_dir.is_dir():
        for path in sorted(bin_dir.iterdir()):
            if not path.name.startswith("."):
                items[f"cli {path.name}"] = "var"
    return items


def main():
    parser = argparse.ArgumentParser(description=__doc__)
    parser.add_argument("--home", type=Path, default=Path.home())
    group = parser.add_mutually_exclusive_group(required=True)
    group.add_argument("--write", type=Path)
    group.add_argument("--baseline", type=Path)
    args = parser.parse_args()
    current = snapshot(args.home)

    if args.write:
        temp = args.write.with_suffix(".tmp")
        temp.write_text(json.dumps(current, ensure_ascii=False, indent=2, sort_keys=True) + "\n")
        temp.chmod(0o600)
        temp.replace(args.write)
        print(f"{len(current)} öğe kaydedildi: {args.write}")
        return 0

    old = json.loads(args.baseline.read_text())
    changes = [f"EKLENDI  {k}" for k in sorted(current.keys() - old.keys())]
    changes += [f"SILINDI  {k}" for k in sorted(old.keys() - current.keys())]
    changes += [f"DEGISTI  {k}" for k in sorted(old.keys() & current.keys()) if old[k] != current[k]]
    print("\n".join(changes) if changes else f"Değişiklik yok ({len(current)} öğe).")
    return 1 if changes else 0


if __name__ == "__main__":
    sys.exit(main())

Betiğin iki önemli davranışı var. Birincisi sır yazmaz: Komut metni kopyaya girmez, MCP sunucularının env ve başlık değerleri okunmaz, yalnız ortam değişkeni adları izlenir. Testte kanarya değeri ne kopyada ne çıktıda göründü. İkincisi hook’un çağırdığı dosyanın içeriğini de özetler: Tanım aynı kalıp betik değişirse DEGISTI satırı çıkar. Sır değerini değiştirmek gürültü üretmedi, bozuk bir JSON dosyası da betiği düşürmedi.

Sınırlarını da bilin:

Kurumda gerçek sınır: merkezden yönetilen yapılandırma

Ekip için dosya izlemek yetmez. Yazma yetkisini ayırmak gerekir. İki aracın belgelediği yol şöyle; bunları dağıtarak denemedim:

AraçSistem dosyasıAnahtarlar
Claude Code/Library/Application Support/ClaudeCode/managed-settings.json, /etc/claude-code/managed-settings.jsonallowManagedHooksOnly, strictKnownMarketplaces, allowManagedMcpServersOnly
Codex/etc/codex/requirements.toml, macOS’ta MDMallow_managed_hooks_only

Bu dosyalar sistem dizininde durduğu için normal kullanıcı süreci yazamaz. Bedeli esnekliktir: Geliştiriciler kendi hook’larını çalıştıramaz; onaylı listeye bir şey eklemek için bir süreç işletmek gerekir. Yönetici yetkisi olan bir süreç bunu da aşabilir, o yüzden yerel yönetici hakkı ayrı bir karardır.

Proje dosyaları için en basit kural, AGENTS.md, CLAUDE.md, .claude/, .codex/, .cursor/ ve .mcp.json dosyalarını koddan farksız görmektir. Bu yollara kod sahibi tanımlayın ve incelemeden geçirin. NVIDIA denemesinde talimat dosyası derleme sırasında yazıldı, yani CI çıktısını da izlemek gerekir.

Geliştirici aracı yapıyorsanız

Postman’in niyeti kurulum zahmetini azaltmaktı, bu anlaşılır bir hedef. Ama başka bir aracın yapılandırmasına yazmak bambaşka bir adımdır. Kendi ürününüz için bence şu kurallar makul:

  1. Her aracı ayrı sorun. “Ajanlarınıza kuralım mı?” değil, hangi ajana ne ekleneceğini gösteren bir onay.
  2. Mevcut kullanıcıya güncellemeyle eklemeyin. Kullanıcı bu özelliği hiç görmemiş olabilir.
  3. Hook onayını kullanıcıya bırakın. Onay mekanizması teknik olarak yazılabilir olsa da o kararı veren taraf siz olmamalısınız.
  4. Başlangıç talimatını dar tutun. Yalnız kullanıcı ürününüzü açıkça istediğinde devreye girsin.
  5. Kaldırmayı eksiksiz yapın. Eklenen her ajan, pazar, hook kaydı, MCP bağlantısı ve CLI aynı işlemle gitsin.

Kontrol listesi

Sonuç

Postman olayının önemi, verdiği zarardan çok gösterdiği şeyde. Ajan yapılandırması artık sıradan bir ayar dosyası değil, bir sonraki oturumda çalışacak kod ve modelin okuyacağı talimattır. Masaüstü uygulaması da, eklenti pazarı da, bir bağımlılık da buraya yazabilir.

Bireysel geliştirici için pratik adım, bu ayarları görünür kılmak. Ekipler için ise soru daha net: Ajanın ne çalıştıracağına kim karar veriyor ve bu kararın yazıldığı dosyaya başka kim yazabiliyor?

Birincil kaynaklar