Dev

Was die Settings-Datei eines geklonten Repos noch ausführen kann

Claude Code hat Repositories gerade verboten, den Telemetrie-Export einzuschalten. Der schmale Fix verdeckt eine breitere Tatsache: Eine eingecheckte Settings-Datei ist Code, der mit deinen Rechten läuft, und in einem Headless-Lauf fragt vorher niemand. Interaktiv fragt zwar ein Dialog, aber ich bestätige ihn, ohne ihn zu lesen.

Claude Code v2.1.282 kam am 24. September mit einer Zeile in den Release Notes, über die man leicht hinwegscrollt: Projekt- und lokale Settings ignorieren jetzt die OpenTelemetry-Variablen, die „den Export einschalten, seinen Endpoint setzen oder Inhalte erfassen“. Rückwärts gelesen sagt sie etwas Unangenehmeres. Bis zu diesem Release konnte eine in ein Repository eingecheckte .claude/settings.json die Telemetrie einschalten, den Collector wählen und verlangen, dass Prompts und Tool-Inhalte mitgeschickt werden. Repo klonen, Session starten, und deine Session-Daten konnten dorthin gehen, wohin der Autor des Repositories sie gelenkt hatte.

Diese Tür ist jetzt zu. Der Rest der Datei steht weiter offen.

Was hat v2.1.282 geändert?

Projekt- und lokale Settings verwerfen jetzt die Telemetrie-Variablen, die den Export einschalten, ihn umleiten oder Inhalte hinzufügen, und dazu kamen ein Hinweis beim Start sowie Einträge in /status und claude doctor, die die verworfenen Variablen nennen. Ausschalten darf ein Repository die Telemetrie weiterhin: none für die drei Exporter-Selektoren und ein Aus-Wert wie 0 für die Schalter, die Prompts, Tool-Inhalte und Tool-Details loggen.

Die vollständige Liste in der Settings-Referenz umfasst CLAUDE_CODE_ENABLE_TELEMETRY, die Exporter-Selektoren, die OTEL_LOG_*-Inhaltsschalter und die OTEL_EXPORTER_OTLP_*-Variablen für Endpoint, Header, Protokoll und Zertifikate. Sie steht neben einer älteren Gruppe, die dieselbe Datei schon vorher nicht setzen durfte: CLAUDE_CONFIG_DIR, HOME, TMPDIR und die XDG_*-Familie, die bestimmen, wohin Claude Code seine eigenen Dateien schreibt, sowie OTEL_LOG_RAW_API_BODIES. Den Großteil dieser früheren Gruppe datiert die Doku auf v2.1.251. v2.1.282 ist also ein weiterer Schritt in einer stetigen Verengung dessen, was ein Repository für dich entscheiden darf.

Ich habe es auf 2.1.283 in einem Wegwerf-Repo ausprobiert, mit einer Projektdatei, die zwei Telemetrie-Variablen setzen wollte, und claude doctor meldete:

text
Claude Code ignores these telemetry variables in .claude/settings.json:
CLAUDE_CODE_ENABLE_TELEMETRY, OTEL_EXPORTER_OTLP_ENDPOINT.
A project's settings files can only turn telemetry off

Das Verhalten vor dem Fix ist hier dokumentiert, nicht nachgestellt: Die „Vorher“-Hälfte stützt sich auf die Doku und die Release Notes.

Falls ein Team sich darauf verlassen hat, dass seine eingecheckten Settings die eigene Telemetrie routen, hat das mit dem Update aufgehört. Eine interaktive Session zeigt den Hinweis beim Start; ein -p-Lauf oder eine SDK-Session zeigt nichts, deshalb rät die Doku zu prüfen, ob der Collector noch Daten bekommt. Die Variablen gehören in die User-Settings, die Managed Settings, die Umgebung des Jobs oder eine Datei, die mit --settings übergeben wird. Keine davon wird aus den Settings-Dateien des Repositories selbst gelesen, und genau darum geht es bei der Änderung; eine --settings-Datei hilft nur, wenn sie außerhalb des Repos liegt.

Was können die Settings eines geklonten Repos noch?

Eine Menge, und das meiste davon wird ausgeführt. Die Permissions-Doku hat eine Tabelle mit dem Titel „What runs before you trust a folder“, und sie ist die nützlichste Seite, die Anthropic zu diesem Thema geschrieben hat. Verdichtet teilt sich die eingecheckte Konfiguration eines Repositories in drei Gruppen:

  • Greift sogar in einem Ordner, dem du nie vertraut hast (unter claude -p oder wenn du nur einem übergeordneten Ordner vertraut hast; Vertrauen in einen Elternordner erstreckt sich nie auf ein verschachteltes Git-Repository wie einen Klon): Hooks in Settings-Dateien, der env-Block ohne die Variablen, die die Settings-Referenz als ignoriert führt, Helper-Befehle wie apiKeyHelper und awsAuthRefresh sowie die Hooks und allowed-tools eines Projekt-Skills. Für Skills ergänzt die Doku, dass Workspace-Trust allowed-tools in keinem Session-Typ absichert.
  • Zurückgehalten, bis du den Trust-Dialog für diesen Ordner bestätigst: permissions.allow-Regeln und additionalDirectories, die Fähigkeiten gewähren, und ein headersHelper auf einem MCP-Server (seit v2.1.238): Für diesen Helper fragt eine interaktive Session noch einmal nach, und bis dahin verbindet sich der Server nur mit seinen statischen Headern. deny- und ask-Regeln gelten sofort, weil sie nur einschränken.
  • Ohne Vertrauen in diesen Ordner nie genutzt, in keinem Session-Typ: Frontmatter-Hooks in einem Projekt-Subagent (seit v2.1.218), inline mcpServers im Subagent-Frontmatter (seit v2.1.238), ein Projekt-Plugin aus @skills-dir und extraKnownMarketplaces. Diese werden übersprungen, nicht abgefragt.

Server in .mcp.json sind ein eigener Fall. Interaktiv fragt Claude Code, bevor es sie verbindet, und seit v2.1.196 kann ein Repository seine eigenen Server nicht selbst freigeben: enableAllProjectMcpServers oder enabledMcpjsonServers in der eingecheckten Projektdatei werden in einem nicht vertrauten Ordner ignoriert. Unter claude -p verbinden sie sich ohne Rückfrage, freigegeben oder nicht.

Schau dir die Versionsnummern in dieser Liste an: 2.1.196, 2.1.218, 2.1.238, 2.1.251, 2.1.282. Jede hat eine Art von Repository-Inhalt aus der Hand des Repositories genommen oder hinter die Trust-Grenze geschoben. Das ist schrittweise Härtung, eine Angriffsfläche nach der anderen, kein Prinzip, das einmal angewendet wurde, und es heißt: Die Version, die du fährst, zählt. Eine alte, gepinnte CLI in einem CI-Image hat noch die Lücken, die die späteren Releases geschlossen haben.

Schützt der Trust-Dialog einen claude -p-Lauf?

Nein. Die Doku sagt es klar: Ein -p-Lauf oder eine SDK-Session zeigt den Dialog nie, und für Hooks und env gilt er als bestätigt. In einem Headless-Lauf greifen die Hooks und die Umgebung des Repositories in einem Ordner, den du nie geöffnet hast.

Hier der Repro. Ein Repository, in dem das als .claude/settings.json eingecheckt ist:

json
{
  "env": {
    "PROBE_VAR": "set-by-repo",
    "CLAUDE_CODE_ENABLE_TELEMETRY": "1",
    "OTEL_EXPORTER_OTLP_ENDPOINT": "http://127.0.0.1:9"
  },
  "hooks": {
    "SessionStart": [
      { "hooks": [ { "type": "command",
        "command": "env | grep -E '^(PROBE_VAR|CLAUDE_CODE_ENABLE|OTEL_EXPORTER)' > /tmp/hook-ran.txt" } ] }
    ]
  }
}

Drei Läufe auf Claude Code 2.1.283, in einem Ordner ohne Trust-Eintrag in ~/.claude.json:

bash
rm -f /tmp/hook-ran.txt
claude -p "Reply with the single word ok" --max-turns 1 < /dev/null
cat /tmp/hook-ran.txt
# PROBE_VAR=set-by-repo
 
rm -f /tmp/hook-ran.txt
claude -p "Reply with the single word ok" --max-turns 1 \
  --settings '{"disableAllHooks": true}' < /dev/null
# no /tmp/hook-ran.txt
 
rm -f /tmp/hook-ran.txt
claude -p "Reply with the single word ok" --max-turns 1 \
  --setting-sources user < /dev/null
# no /tmp/hook-ran.txt

Sieh dir den ersten Lauf an. Der Hook des Repositories lief, bevor das Modell ein Wort gesagt hatte, er sah die PROBE_VAR des Repositories, und er sah die beiden Telemetrie-Variablen nicht, weil v2.1.282 sie verworfen hat. Der Hook hier schreibt eine Datei. Ein echter könnte alles ausführen, was dein Account ausführen darf, mit allen Credentials, die deine Umgebung hält. Das ist der Punkt, den ich über installierte Tools mit deiner Reichweite gemacht habe: Die Berechtigung ist ambient, und ein Hook erbt sie komplett.

Was hält eine interaktive Session zurück, bis du dem Ordner vertraust?

Alles, was ausgeführt wird, bis du Ja sagst. Laut Hooks-Doku hält eine interaktive Session die Hooks aus allen Settings-Dateien zurück, auch aus deiner eigenen ~/.claude/settings.json, bis du den Trust-Dialog für den Ordner bestätigst oder für einen Elternordner, dessen Vertrauen sich auf ihn erstreckt. Der Dialog listet die Allow-Regeln und zusätzlichen Verzeichnisse auf, die der Ordner gewähren würde, damit du sie vorher prüfen kannst. Nach der Bestätigung greift alles, Hooks und Helper eingeschlossen.

Interaktiv ist also der Dialog das Tor, und hier muss ich ehrlich über mein eigenes Setup sein. Meine Agents laufen als interaktive Claude-Code-Sessions in tmux, nicht als -p-Skripte. Das war schon so, bevor sich beim Agent-SDK-Guthaben etwas geändert hat, und ich lasse -p nicht über Repositories laufen, die ich nicht selbst geschrieben habe. Die Headless-Lücke oben ist also nicht meine Angriffsfläche. Der Dialog ist es. Und ich lese ihn nicht. Ich habe noch nie die .claude/settings.json oder .mcp.json eines Repositories geöffnet, bevor ich Vertrauen bestätigt habe, und es hat mich nie erwischt, was über das nächste Repo nichts beweist.

Ein Tor, durch das du aus Reflex klickst, ist ein Tor auf dem Papier. Der Dialog erfüllt seinen Zweck nur, wenn die Liste, die er zeigt, gelesen wird, und eine Liste von Berechtigungen in einem Modal, genau in dem Moment, in dem du anfangen willst zu arbeiten, ist genau das, was ich ungelesen bestätige.

Wie prüfst du ein Repo, bevor du ihm vertraust?

Lies die Dateien, die der Dialog zusammenfasst, bevor der Dialog erscheint, mit einem Skript, das nur die Schlüssel herauszieht, die etwas ausführen oder gewähren. Das ist schlichtes Python ohne Abhängigkeiten. Starte es im Root-Verzeichnis des Repositories:

python
import json, pathlib, subprocess
 
KEYS = ["env", "hooks", "apiKeyHelper", "awsAuthRefresh", "awsCredentialExport",
        "otelHeadersHelper", "statusLine", "subagentStatusLine", "fileSuggestion",
        "enabledPlugins", "extraKnownMarketplaces"]
 
for name in [".claude/settings.json", ".claude/settings.local.json"]:
    p = pathlib.Path(name)
    if p.exists():
        s = json.loads(p.read_text())
        found = {k: s[k] for k in KEYS if k in s}
        perms = s.get("permissions", {})
        for k in ["allow", "additionalDirectories"]:
            if perms.get(k):
                found["permissions." + k] = perms[k]
        print(name, json.dumps(found, indent=2))
 
mcp = pathlib.Path(".mcp.json")
if mcp.exists():
    for n, srv in json.loads(mcp.read_text()).get("mcpServers", {}).items():
        print("mcp server:", n, json.dumps(srv))
 
for d in [".claude/agents", ".claude/skills", ".claude/commands"]:
    if pathlib.Path(d).is_dir():
        print("read the files in", d)
 
r = subprocess.run(["git", "ls-files", ".claude/settings.local.json"],
                   capture_output=True, text=True)
if r.returncode != 0:
    print("git check failed:", r.stderr.strip())
elif r.stdout.strip():
    print("settings.local.json is tracked in git: treat it as repository-supplied")

Auf meinem ersten Test-Repository, das auch eine .mcp.json mit einem Server enthielt, gab das Skript den env-Block aus, den SessionStart-Hook mit seinem vollständigen Befehl und die vollständige npx-Definition dieses Servers. Es ist ein erster Durchgang, kein Unbedenklichkeitsnachweis: Es liest nur das Root-Verzeichnis des Repositories, und bei Agents, Skills und Commands sagt es dir nur, welche Ordner du öffnen musst. Was es ausgibt, ist eine Liste von Befehlen und Berechtigungen, die du lesen solltest, bevor du sie laufen lässt.

Die letzte Prüfung ist wichtiger, als sie aussieht. Eine .claude/settings.local.json gehört normalerweise dir, deshalb überspringen ihre Allow-Regeln den Trust-Schritt, sobald Claude Code sie mit git geprüft hat. Wenn die Datei in git getrackt ist oder .claude ein Symlink ist, behandelt Claude Code sie als vom Repository geliefert und hält ihre Regeln zurück wie bei der geteilten Datei. Ein Repository, das eine „lokale“ Datei eincheckt, verrät dir etwas über sich.

Das ist dieselbe Grenze, für die ich bei Instruction-Dateien als Adressierung argumentiert habe: Text, den ein Fremder einchecken kann, darf nie die Autorität deiner eigenen dauerhaften Konfiguration tragen. Settings sind diese Grenze mit ausführbaren Zähnen.

Wie lässt du claude -p in einem fremden Repository laufen?

Entscheide vor dem Start, was der Lauf laden darf. Die Doku nennt vier Optionen, und sie sind nicht austauschbar:

  1. --setting-sources user liest weder die Settings-Dateien des Projekts noch seine .mcp.json. In meinem Repro hat das den Hook gestoppt. Es ist der breiteste einzelne Schalter.
  2. --bare überspringt Hooks, Skills, eigene Commands, Subagents, Plugins und .mcp.json-Server. Den env-Block des Projekts und Helper wie awsAuthRefresh lässt es aber nicht weg, und es liest keinen OAuth-Login, auf der Anthropic API brauchst du also ANTHROPIC_API_KEY oder einen apiKeyHelper, der über --settings übergeben wird. Skills aus einem Verzeichnis, das du mit --add-dir übergibst, werden trotzdem geladen. Laut Doku wird es in einem künftigen Release zum Standard für -p.
  3. --settings '{"disableAllHooks": true}' schaltet für den Lauf die normalen User-, Projekt- und Plugin-Hooks ab; Hooks, die deine Organisation verwaltet, bleiben an. Es muss auf der Kommandozeile stehen: Steht es nur in deinen User-Settings, verliert es, weil Projekt-Settings Vorrang vor User-Settings haben und das Repository es wieder auf false setzen kann. In meinem Repro hat das Flag den Hook gestoppt.
  4. disabledMcpjsonServers lehnt einen benannten .mcp.json-Server in jedem Session-Typ ab, aus jeder Settings-Datei.

Meine Einschätzung, wozu du greifen solltest: --setting-sources user für alles Automatisierte über Code, den du nicht geschrieben hast, weil es als einzige der vier Optionen env, Hooks und Server des Projekts zusammen entfernt. --bare ist schneller und sauberer für CI, aber „überspringt Hooks“ heißt nicht „überspringt das Repository“, und der env-Block ist genau der Teil, den --bare stehen lässt.

Was solltest du diese Woche tun?

Aktualisiere die CLI überall, wo sie über fremden Code läuft, auch in CI-Images mit gepinnter Version. Ergänze --setting-sources user oder --bare bei jedem geskripteten claude -p über fremde Repositories; die Doku sagt schon, dass --bare zum Standard für -p wird, und das ist dieselbe Richtung wie jede Versionsnummer oben. Bevor du den nächsten Trust-Dialog bestätigst, lass das Audit laufen, oder öffne wenigstens .claude/settings.json und .mcp.json und lies die Befehle darin.

Die Telemetrie-Variablen waren der einfache Fall: Sie senden nur. Die Schlüssel, die etwas ausführen, stehen noch in der Datei. Zwischen den Hooks eines Repositories und deiner Shell steht im einen Modus ein Dialog, und im anderen nichts.

Diskussion

Hier gibt es keine Kommentarspalte. Diskussionen laufen auf X.

Max Nardit

Max Nardit

@mnardit

Weitere Artikel

Opus 5.5 ist günstiger, und die 400er sind der einfache Teil

Ein Request, der 400 zurückgibt, hat dir schon gesagt, was du reparieren musst. Die Änderungen, die dich bei diesem Upgrade etwas kosten, kommen als 200 zurück: eine Antwort, die nicht mehr mit Text beginnt, ein Effort-Level, das eine Stufe tiefer liegt, weil du es nie gesetzt hast, und ein Alias, der dich ohne Deploy auf ein neues Modell umgestellt hat.

Ein Preis, den man nicht ausrechnen kann

Die Rechnung steigt um $96 im Jahr, das ist nichts. Darunter hat sich etwas anderes verändert: Die Zahl lässt sich nicht mehr herleiten. Das im Plan enthaltene Kontingent hat keine veröffentlichte Größe, das Gratis-Kontingent an Credits auch nicht, und der Jahres-Credit kostet mehr als der Monats-Credit. Ein Preis, den du nicht aus veröffentlichten Zahlen ausrechnen kannst, ist eine Schätzung, und eine Schätzung ist eine andere Art von Abhängigkeit.

Claude Code liest jetzt AGENTS.md, und der Standard ist ein Fallback

Anthropic hat wieder das Laden gelöst und die Rangfolge gelassen, wo sie war. Welche Projektdatei der Standard lädt, hängt davon ab, was zufällig auf der Platte liegt. Die geladene Datei fehlt in den Übersichten, mit denen du sie prüfen würdest, und der alte Einzeiler-Import bleibt das Setup, das sich über Provider und Versionen hinweg gleich verhält.