Zum Hauptinhalt springen

Mods: In-Process-Erweiterungen

Experte

Ein Mod ist ein Plugin, das JavaScript- oder TypeScript-Funktionen mitbringt, die Claude Code im eigenen Prozess aufruft. Während ein Hook ein Shell-Befehl ist, der ein JSON-Payload erhält und mit einem Exit-Code antwortet, sitzt der Handler eines Mods in einer Middleware-Kette vor dem eigenen Verhalten von Claude Code — er kann ein Event beobachten, umschreiben oder beantworten, sodass das eingebaute Verhalten nie läuft.

Genau diese letzte Fähigkeit ist der Punkt. Mehrere Features von Claude Code selbst sind inzwischen Mods: /diff und das Laden von AGENTS.md werden als cc-plugin-diff und cc-plugin-agents-md ausgeliefert, mit Quellcode im mods/-Verzeichnis des Claude-Code-Repos. Wenn der Hersteller einen eingebauten Befehl durch einen Mod ersetzen kann, kannst du das auch.

What you'll learn
  • Das Middleware-Modell: ein Event beobachten, umschreiben oder beantworten — und warum next(e) die Engine selbst erreicht
  • Die drei Dateien, die ein Mod braucht, und die Edit-Reload-Schleife mit --plugin-dir
  • Das Gate der statischen Analyse: warum const ui = $.ui ein Ladefehler ist und keine Stilfrage
  • Die Event-Karte (Tools, Prompts, Turns, Session, UI) und die $-Namespaces, die ein Hook aufrufen kann
  • Fünf Fallen, die in der Produktion zuschlagen: fail-open-Hooks, der gemeinsame Worker, das 10-Sekunden-Budget, State-Resets, die Asymmetrie der Deny-Regeln
  • Was ein Mod auf deinem Rechner erreichen kann, wie du einen vor der Installation prüfst und mit welchen Managed Settings ein Admin sie einzäunt

Mod, Hook, Skill oder MCP-Server?​

Alle vier erweitern Claude Code und sie überlappen sich. Wähle danach, was du ändern musst:

ModSettings-HookSkillMCP-Server
Was es istFunktionen in einem Plugin, die im Prozess von Claude Code laufenEin Shell-Befehl / HTTP-Request / Prompt, ausgelöst von einem Lifecycle-EventEine SKILL.md mit AnweisungenEin externer Prozess, der Tools bereitstellt
Was es ändern kannTool-Aufrufe, Prompts, Turns, Befehle und was die Oberfläche zeichnetOb eine Aktion fortgesetzt wird; Argumente und Ergebnis eines Tools; Kontext für ClaudeWas Claude weiß und tutWelche Tools Claude hat
Kann es UI zeichnen?JaNeinNeinNein
Du schreibstJS / TSEin Skript + einen Eintrag in settings.jsonMarkdownEinen Server, in beliebiger Sprache
Greif danach, wennDu ein Pane, ein Band über dem Prompt, einen sofortigen /command willst oder ein Event umschreiben möchtestDu schon ein Skript hast, das blocken, erlauben oder loggen sollDu immer wieder dieselben Anweisungen einfügstClaude ein externes System braucht

Nichts ist veraltet: Settings-Hooks laufen weiterhin, parallel zu Mods. Ein Plugin kann einen Mod, einen Skill, Befehle und einen MCP-Server zusammen mitbringen — siehe Plugins & Marketplaces.

Das Middleware-Modell​

Jeder Hook erhält dieselben drei Argumente — die Mods-API als $, das eingefrorene Event als e und next. Die Handler für ein Event bilden eine Kette; das letzte next ist das eigene Verhalten von Claude Code. Was du mit next machst, entscheidet, welche der drei Rollen du spielst.

ModusWieWirkung
Beobachtenreturn next(e)Das Event läuft unverändert weiter. Mit await next(e) zuerst, um danach zu handeln.
Umschreibenreturn next({ ...e, text })Spätere Handler und die Engine sehen deine Kopie und niemals das Original.
Beantwortenein Ergebnis zurückgeben, next nie aufrufenKappt die Kette: spätere Mods und das eingebaute Verhalten laufen nicht.
// Observe: log the tool, then let it run
on('tool.call', async ($, e, next) => {
$.ui.log('Claude is about to use ' + e.tool)
return next(e)
})

// Rewrite: trim every prompt before the turn starts
on('prompt.submit', async ($, e, next) => next({ ...e, text: e.text.trim() }))

// Answer: refuse force-pushes, and tell Claude why
on('tool.call', { tool: 'Bash' }, async ($, e, next) => {
if (/git push .*--force/.test(e.command)) {
return { deny: 'Force pushes are not allowed here. Push to a new branch instead.' }
}
return next(e)
})

Zwei Dinge musst du verinnerlichen. Erstens: e ist tief eingefroren — eine Zuweisung an ein Feld wirft einen Fehler; du übergibst eine Kopie an next. Zweitens: Wenn du einen tool.call beantwortest, ohne next aufzurufen, erscheint keine Berechtigungsabfrage und das Tool läuft nie, sodass alles, was du zurückgibst, das Einzige ist, was Claude über das Geschehene erfährt.

Das zweite Argument von on ist ein Matcher: ein Objekt, das Feld für Feld gegen das Event verglichen wird. Ein Feld kann ein Wert, ein Array oder eine Regex sein.

on('tool.call', { tool: 'Bash' }, hook) // one tool
on('tool.call', { tool: ['Edit', 'Write'] }, hook) // any of these
on('tool.call', { tool: /^mcp__github__/ }, hook) // one MCP server's tools

'*' passt auf jedes Event außer den Telemetrie-Events. Registriere jedes Event einmal pro Matcher — zwei on('session.start', …)-Aufrufe ohne Matcher lassen das Modul mit on("session.start") is registered twice without a matcher am Laden scheitern.

Einen Mod in drei Dateien bauen​

Kein Node.js, kein Bundler, kein Build-Schritt — Claude Code lädt .js und .ts direkt.

Guided walkthrough1 of 6
  1. mkdir -p first-mod/.claude-plugin first-mod/hooks — ein Mod IST ein Plugin, also braucht er das normale Plugin-Manifest unter .claude-plugin/plugin.json.

Der kanonische Einstieg zählt Tool-Aufrufe und zeigt den Zähler neben dem Spinner:

// hooks/register.js — shared by every hook in this module
let calls = 0

export function register(on) {
on('session.start', async ($, e, next) => {
await $.command.register({ name: 'tally', description: 'Show how many tool calls Claude has made' })
return next(e)
})

on('tool.call', async ($, e, next) => {
calls += 1
$.ui.invalidate('ui.render') // ask for a redraw so the new count shows
return next(e)
})

on('command.run', { command: 'tally' }, async () => {
return { text: 'Claude has made ' + calls + ' tool calls since this mod loaded' }
})

on('ui.render', { component: 'Spinner' }, async ($, e, next) => {
return next({ ...e, props: { ...e.props, suffix: ' · tool calls: ' + calls + '…' } })
})
}

Die gemeinsam genutzte Variable calls auf Modulebene ist das idiomatische Muster: ein Hook sammelt, ein anderer zeigt an. Genau das können Settings-Hooks strukturell nicht leisten, weil jeder davon ein eigener Prozess ist.

Du musst das nicht von Hand schreiben. Claude Code bringt einen eingebauten plugin-authoring-Skill mit (in /plugin als cc-plugin-plugin-authoring aufgeführt), sodass es genügt, den Mod zu beschreiben:

Claude den Mod schreiben lassen

make a mod that shows the current git branch above the prompt

Claude schreibt ihn unter ~/.claude/dev-mods/<session-id>/<mod-name>/, fragt einmal, ob Hot Reloading für die Session aktiviert werden soll, und lädt ihn am Ende jedes Turns neu, der seine Dateien verändert. Achtung, Falle: Dieser Ordner gilt pro Session und Claude Code löscht ihn, sobald er älter als cleanupPeriodDays ist. Kopiere das Verzeichnis an einen eigenen Ort, um den Mod zu behalten.

Die Fähigkeiten eines Mods prüfen, bevor du ihn ausführst

claude plugin validate ./some-mod

Das Gate der statischen Analyse​

Das ist der Teil, der erfahrene JS-Entwickler überrascht, und der wichtigste Mechanismus auf dieser Seite. Claude Code weigert sich, einen Mod zu laden, dessen Verwendung von $ es nicht statisch lesen kann. Genau das macht claude plugin validate zu einem vertrauenswürdigen Fähigkeiten-Manifest — der Loader muss deinen Code nicht ausführen, um zu wissen, was er kann.

Diese Varianten scheitern also an der Validierung:

Du hast geschriebenFehler
const ui = $.ui$.ui is used as a value
on(eventName, hook) mit einer Variable oder einer Schleife über Namenthe event name passed to on() is not a string literal
Ein zweites on, innerhalb von register deklariert"on" is declared again (shadowed)
await import('./other.js')a dynamic import(); a hooks module imports its own files with an import declaration
require(...) oder ein nackter Import außer claude-codeModul ist kein gültiges Hooks-Modul
on('tool.calls', …)"tool.calls" is not an event

Schreibe jeden Aufruf vollständig — $, Namespace, Methode — wie in $.store.get('notes'). Du darfst $ an eine Funktion übergeben, die auf der obersten Ebene derselben Datei deklariert ist; validate ordnet den Aufruf dann zu, z. B. $.store.get (via loadNotes). $ in eine Methode, in eine innerhalb des Hooks definierte Closure oder in eine importierte Funktion zu übergeben, scheitert.

Die Ausgabe, die du liest, sieht so aus:

❯ ./register.js hooks: session.start, tool.call, command.run{command=tally}, ui.render{component=Spinner}
❯ ./register.js calls: $.command.register, $.ui.invalidate

✔ Validation passed

Mods, die Umgebungsvariablen oder $.state anfassen, erhalten zusätzlich env reads: / env writes: / state reads: / state writes:-Zeilen. --strict macht aus Warnungen Fehler; --json macht die Ausgabe maschinenlesbar, was du in der CI willst.

Die Event-Karte​

Events feuern an jedem Punkt, an dem Claude Code gerade handeln will. Der vollständige Satz bei v2.1.287, gruppiert:

GruppeEventsEin Hook kann typischerweise zurückgeben
Toolstool.call, tool.check, tool.describenext(e), { deny: reason }, { result }; tool.check gibt { decision } = allow / ask / deny zurück
Prompts & Kontextprompt.submit, prompt.fill, prompt.suggest, prompt.edit, prompt.compose, prompt.section, prompt.context, prompt.attachment, skill.prompt, attribution.textGeänderten Text, { sections }, { blocks }, { text: null } zum Auslassen, { drop: reason }
Befehle & Konfigurationcommand.run, command.describe, config.set, config.describe{ text }, { description, argumentHint, isHidden }, { deny: reason }
Turnsturn.start, turn.step, turn.completeturn.step ist ein Async-Generator — yield* next(e); er kann model oder effort pro Request austauschen
Sessionsession.start, session.end, session.compact, session.send, session.receive, session.append, session.attach, session.detach, session.measure{ skip: reason } bei compact, { consumed: reason } bei receive, umgeschriebene Zeilen bei append
Subagentenagent.offer, agent.spawn{ isOffered: false }, um einen Typ zurückzuhalten, { model } oder { deny: reason }
Oberflächeui.render, ui.resolve, ui.press, ui.input, ui.select, ui.focus, ui.scroll, ui.close, ui.messageEinen Elementbaum oder next mit geänderten Props
Andere Modsplugin.register, engine.create{ refuse: reason }, um das Laden eines anderen Mods zu verhindern
Telemetrietelemetry.log, telemetry.markErfordert den Matcher { to: 'collector' }, sonst scheitert validate; '*' passt auf diese nicht

Zwei Arten von Events übersieht man leicht:

  • classic.<Event> — jedes Settings-Hook-Event ist auch ein Mod-Event, also funktionieren on('classic.Stop', …) oder on('classic.PostToolUse', …), und e ist dasselbe JSON, das ein Settings-Hook auf stdin liest, transcript_path inklusive. 'classic.*' passt auf alle davon.
  • Jede Methode der Mods-API ist selbst ein Event, benannt ohne das $. — fs.read, model.complete, ui.open. Ein Hook auf eine davon fängt diesen Aufruf bei jedem Mod ab, der danach läuft, und kann next(e), { deny: reason } oder { value } zurückgeben. Das ist der Hook, mit dem ein Policy-Mod einer Organisation prüft oder verhindert, was andere Mods tun.

Die echten Token-Zahlen lesen​

turn.step streamt, also ist sein Hook ein Async-Generator. Das ist der günstigste ehrliche Weg, deine eigene Prompt-Cache-Trefferquote pro Request zu sehen:

on('turn.step', async function* ($, e, next) {
const result = yield* next(e)
if (result.usage) {
$.ui.log('cache read ' + result.usage.cache_read_input_tokens +
' · wrote ' + result.usage.cache_creation_input_tokens)
}
return result
})

result.usage trägt input_tokens, output_tokens, cache_read_input_tokens, cache_creation_input_tokens und das model, das geantwortet hat. Der Hook feuert auch für die Requests von Subagenten, filtere also auf e.agentId, wenn du nur den Haupt-Thread willst. Und beachte die Konsequenz für alle, die prompt.section oder prompt.context umschreiben: Text aus diesen Hooks, der sich zwischen Requests ändert, invalidiert den Prompt-Cache.

Die $-Namespaces​

NamespaceMethoden
$.pluginname, root
$.uiresolve, invalidate, open, close, panes, focus, scroll, toast, status, log, notice, ask, copy, blit
$.command / $.tool / $.agentregister, run/call, list ($.tool zusätzlich check)
$.modelcomplete, fork, classify
$.promptsubmit, read, fill, suggest, compose
$.turnabort
$.sessionmessages, cwd, root, model, turns, id, repo, surfaces, usage, version, compact, send, append, authorize
$.config / $.settings / $.envlist/set, read, get/set
$.fsread, write, list, exists, stat, ancestors
$.storeget, set, delete, keys — maschinenweit, von jeder Session gemeinsam genutzt
$.stateget, set, dazu atom, read, update, derive, memberOf, importiert aus claude-code
$.clock / $.http / $.process / $.mcpnow, sleep, after, every · fetch · run, spawn · call, connect
$.audio / $.telemetryplay, speak · log, mark

$.session.usage() ist die eine Methode, die man namentlich kennen sollte: Sie gibt { startedAt, context, rateLimits, cost } zurück, wobei context die Felder tokens, window und percent hat und rateLimits eine Liste von { kind, percentUsed, resetsAt } ist. Das ist eine Plan-Limit-Anzeige in etwa vier Zeilen — siehe Context Budget Audit dazu, was man mit den Zahlen macht.

Zeichnen​

Ein ui.render-Hook gibt einen Elementbaum an einem Render-Site zurück. Zu den Sites gehören Pane und AbovePrompt (deine eigene Fläche) sowie diejenigen, mit denen du umgestalten kannst, was Claude Code bereits zeichnet: UserMessage, AssistantMessage, ToolUse, ToolResult, ToolGroup, CommandOutput, AskUserQuestion, Spinner, TurnDuration, InfoNotice, SessionMode, PromptHint und das nur im Terminal verfügbare ToolProgress.

Elemente sind Box, Text, Button, Link, Code, Markdown, Input, Select, Client, dazu Svg (nur Desktop) und Raster / Image (nur Terminal). Passe den Baum an seinen Site an, indem du e.props.bodyColumns für die Breite und e.props.scroll.bodyRows für die Höhe liest, wenn e.props.placement gleich 'dock' ist.

Wo dein Mod tatsächlich erscheint, ist nicht dort, wo seine Hooks laufen. Diese Tabelle solltest du prüfen, bevor du einem Teammitglied ein Pane versprichst:

Wo du Claude Code ausführstHooks laufenZeichnen erscheint
claude in einem Terminal (inkl. Editor-Terminals, JetBrains-Plugin)JaJa
Desktop-App, Tab CodeJaJa, ohne die nur im Terminal verfügbaren Elemente
Desktop-App, WSL-SessionNein — Plugins sind dort nicht verfügbarNein
Chat-Panel der VS-Code-ErweiterungJaNein
claude -p und das Agent SDKJaNein
Remote Control von claude.ai / MobilJa, in der Session auf deinem RechnerIn diesem Terminal
Cloud-SessionJa, wenn das Plugin sie erreichtNein

Ein Mod, der zeichnet, sollte also seine Surface prüfen und auf eine Transkriptzeile oder die Textantwort eines Befehls zurückfallen.

Fünf Fallen, die in der Produktion zuschlagen​

Jede davon ist dokumentiertes Verhalten, kein Bug — und jede hat schon jemandem die Finger verbrannt.

1. Ein Guard-Hook scheitert offen. Wenn ein Hook einen Fehler wirft, in ein Timeout läuft oder die falsche Form zurückgibt, bevor er next aufruft, überspringt Claude Code ihn und der nächste Handler läuft an seiner Stelle — was bedeutet, dass der Befehl, den du abgesichert hast, läuft. Wenn er nach dem Auflösen von next gescheitert ist, bleibt dieses Ergebnis bestehen. Um geschlossen zu scheitern, hänge ein .catch an:

on('tool.call', { tool: 'Bash' }, guard).catch(async ($, e, next) => {
// next.error.kind is 'throw' or 'timeout'
return { deny: 'The command guard failed, so this command was not run: ' + next.error.kind }
})

2. Das 10-Sekunden-Budget zählt deine Awaits, nicht die der API. Ein Hook bekommt pro Event 10 s eigene Ausführungszeit, ohne die Zeit innerhalb von next oder innerhalb eines Mods-API-Aufrufs außer $.clock.sleep. await $.ui.ask(...) kann einen Tool-Aufruf also unbegrenzt halten, während der Nutzer entscheidet — aber await myOwnPromise() verbraucht das Budget, und ein Hook, der in ein Timeout läuft, wird übersprungen, sodass der gehaltene Befehl läuft. Lies next.budget.remainingMs, wenn du wissen musst, wie du dastehst.

3. Installierte Mods teilen einen Worker-Thread. Ein Hook, der ihn blockiert — eine Schleife ohne Await — führt dazu, dass dein Mod mit it crashed the hooks worker entladen wird. Stirbt der Worker dreimal und Claude Code kann die Abstürze nicht einem einzelnen Mod zuordnen, entlädt es jeden nicht eingebauten Mod für die Session, den eurer Organisation inklusive, bis jemand /reload-plugins ausführt.

4. State wird auf drei verschiedene Arten zurückgesetzt. Variablen auf Modulebene werden bei jedem Reload neu initialisiert (calls fällt also auf 0 zurück, wann immer du speicherst). $.state fällt nach /clear, /resume oder /branch auf die Defaults zurück — und session.start feuert dabei nicht erneut, lade gespeicherte Werte also stattdessen aus einem classic.SessionStart-Hook. Alles, was mehrere Sessions schreiben, gehört in $.store, nicht in $.fs.write, denn $.fs.write ist nicht atomar und ein anderer Prozess kann eine halb geschriebene Datei lesen.

5. Deine Änderungen erreichen eine installierte Kopie nicht. Claude Code cacht ein installiertes Plugin nach Version, entwickle also gegen --plugin-dir und erhöhe die Manifest-Version, bevor du neu installierst.

Zwei kleinere, die man kennen sollte: Ein nicht vom Nutzer angefordertes $.ui.open braucht beim ersten Mal 144 Terminalspalten (110, sobald der Nutzer dieses Pane selbst einmal geöffnet hat), und $.session.messages() gibt nur die neuesten 4.096 Einträge zurück.

Limits​

LimitWert
Hook-Ausführungszeit, ein Event10 s
.catch-Handler1 s
Alle session.end-Hooks zusammen1,5 s
Timeout von $.process.run30 s Standard, 10 min Maximum
maxTokens bei $.model.complete1024 Standard, bis zu 64.000 oder dem Output-Limit des Modells
$.fs.read / $.fs.write, eine Datei4 MiB
$.store, insgesamt4 MiB JSON
$.session.messages()neueste 4.096 Einträge
ui.render-Neuzeichnungengedrosselt auf 10/s (30/s im Terminal für das sichtbare Pane, das ausgeklappte Band, die Hinweiszeile)
Ein String-Child von Text10.000 Zeichen
Text in Code / Markdown10.000 Zeichen · Svg 131.072 · Image 2 MiB
Rastercolumns ≤ 512, rows ≤ 256
Namen von Befehlen / Tools / Agenten / PanesBuchstaben, Ziffern, _, -, ≤ 64 Zeichen
Ein claude plugin test-Test5 s, sofern er nicht timeoutMs setzt

Sicherheit: was ein Mod tatsächlich erreicht​

Mods laufen nicht in einer Sandbox

Ein Mod läuft im Prozess von Claude Code, mit deinen Berechtigungen. Einmal geladen, kann er jede Datei lesen und schreiben, die dein Konto darf, Programme starten, Netzwerkanfragen stellen, deine Umgebungsvariablen und Settings-Dateien lesen (inklusive eines API-Keys, der in einer von beiden liegt), jeden Prompt und jeden Tool-Aufruf sehen, sie umschreiben, einen Prompt einreichen, als hättest du ihn getippt, einen Tool-Aufruf genehmigen, bevor du gefragt wirst, und über $.model.complete dein Plan- oder API-Guthaben ausgeben. Wenn du Sandboxing aktivierst, isoliert das die Bash-Befehle, die Claude ausführt — ein von einem Mod gestarteter Prozess läuft außerhalb davon.

Es gibt eine Asymmetrie, die eine eigene Zeile verdient, weil sie das verbreitetste mentale Modell aushebelt: deny-Berechtigungsregeln binden Claudes Tool-Aufrufe, nicht die eigenen Aufrufe eines Mods. Mit verweigertem Read(.env) kann ein Mod diese Datei dennoch mit $.fs.read lesen oder ein Programm starten, das es tut. Berechtigungsregeln sind keine Eingrenzungsgrenze für Mod-Code. Ein Mod kann auch nicht die Berechtigungsabfrage selbst umgestalten — diese eine Oberfläche ist tabu, was eine Abfrage dir also zeigt, ist immer echt.

Die praktische Verteidigung ist die Audit-Zeile, nicht Vertrauen: Führe claude plugin validate auf dem Verzeichnis aus und lies calls:. Das sind die Einträge, bei denen du stoppen und den Quellcode lesen solltest — $.fs.read / $.fs.write, $.process.run / $.process.spawn, $.http.fetch, $.env.get / $.settings.read (API-Keys), $.env.set (ändert, was jeder spätere Befehl und MCP-Server ausführt), $.model.complete (dein Guthaben), $.prompt.submit (kann Text als die eigenen Worte des Nutzers einreichen), $.session.send (speist Claude in einer anderen Session). In der hooks:-Zeile bedeuten tool.call und prompt.submit, dass er alles sieht und alles ändern kann; session.append bedeutet, dass er gespeicherte Konversationszeilen umschreiben kann; tool.check bedeutet, dass er erlauben oder verweigern kann, bevor irgendeine Abfrage erscheint.

Anthropic veröffentlicht Beispiel-Mods in claude-code-playground — token-weather (Prognose zum Context Window über dem Prompt), blast-radius (hält einen riskanten Shell-Befehl zurück und zeigt, was er ändern würde), replay-theater (ein /replay-Befehl, der durch die Edits des Turns schrittweise führt). Geteilt wie sie sind, ohne Support, und sie laufen mit deinen Berechtigungen wie alles andere. Dieselbe Review-Disziplin wie in Reviewing Third-Party Code.

Sie abschalten​

UmfangWie
Ein ModSein Plugin im Tab Installed in /plugin deaktivieren oder deinstallieren
Jeder installierte Mod, eine Sessionclaude --safe-mode (deaktiviert auch deine übrigen Anpassungen)
Jeder installierte Mod, dauerhaft"disableAllHooks": true in ~/.claude/settings.json — deine Settings-Hooks und die eigene Statuszeile stoppen damit ebenfalls

Eingebaute Mods sind von keinem davon betroffen; jeder hat seinen eigenen Schalter in /plugin. Um zu testen, ob Mods überhaupt laden können, führe claude plugin test in einem Verzeichnis ohne Mod aus: no hooks module to load heißt ja; hooks modules are turned off here heißt, dass eine Einstellung oder eine Organisationsrichtlinie sie blockiert; hooks modules are turned off in this process heißt, dass Anthropic installierte Mods aus der Ferne deaktiviert hat und nichts Lokales sie wieder aktiviert.

Wenn du während des Early Access CLAUDE_CODE_ENABLE_FUNCTION_HOOKS gesetzt hast, entferne es — v2.1.287+ ignoriert es bei jedem Wert, eine 0 dort hält Mods also nicht aus.

Die Seite der Organisation​

Auf einem Rechner mit Managed Settings oder bei einer Team-/Enterprise-Anmeldung lädt Claude Code einen eingebauten Guard — sec-default@builtin, angezeigt als cc-plugin-sec-default — vor jedem nutzerinstallierten Mod. Standardmäßig schützt er nur, was die Organisation verwaltet (Eingaben und Entscheidungen verwalteter Hooks, den System-Prompt, verwaltete Anweisungen, Settings so wie jeder Mod sie liest, verwaltete MCP-Tools) und erzwingt, dass der Mod eines Nutzers keinen Aufruf genehmigen kann, den eine deny-Regel verweigert. Alles andere bleibt erlaubt. Sein Quellcode ist öffentlich in mods/sec-default.

Ziel des AdminsManaged Setting
Keine nutzerinstallierten Mods, Hooks unberührtallowManagedModsOnly: true unter pluginConfigs → cc-plugin-sec-default@builtin → options
Nur die Mods der Organisationdas Obige + sie so installieren, dass sie als die der Organisation zählen
Jeder Mod, aber nur aus genehmigten MarketplacesMarketplace-Beschränkungen beibehalten + disableSideloadFlags: true
Jeder Mod, überwacht vom Mod der Organisationden Mod der Organisation zusammen mit sec-default@builtin in prependPlugins aufführen
Gar nichts, verwaltete Hooks inklusivedisableAllHooks: true

Der Guard liest seine Optionen ausschließlich aus Managed Settings, und ausschließlich unter dem Schlüssel cc-plugin-sec-default@builtin (prependPlugins akzeptiert auch die Kurzform sec-default@builtin; pluginConfigs nicht), und er scheitert geschlossen — kann er die Managed Settings nicht lesen, verweigert er jeden Nutzer-Mod. Wenn du prependPlugins selbst setzt, nenne den Guard in der Liste, sonst wird er nicht mehr geladen.

Ein Policy-Mod erhält plugin.register für jeden Mod, der geladen werden soll, mit e.tier (prepend / user / append / builtin) und e.uses.calls — dieselbe Liste, die validate ausgibt, nur ohne das $. geschrieben. Ein zurückgegebenes { refuse: reason } verhindert das Laden dieses Mods. Beachte dieselbe Fail-Open-Falle wie oben: Wenn deine Prüfung einen Fehler wirft, lädt der geprüfte Mod, packe die Prüfung also in eine benannte Funktion und ergänze ein .catch, das den user-Tier verweigert.

Debugging​

Wo eine Fehlerzeile erscheint, hängt von der Session ab — deshalb ist „mein Mod tut nichts“ so verbreitet:

  • Hot Reloading eines Plugin-Verzeichnisses (--plugin-dir oder von Claude geschriebene Mods mit aktivem Hot Reload): eine gedimmte Zeile im Transkript.
  • Jede andere interaktive Session, inklusive eines aus einem Marketplace installierten Mods: nur das Debug-Log — starte mit claude --debug.
  • claude -p --plugin-dir: stderr im Standard-Textformat; die Ablehnung eines anderen Mods geht nur ins Debug-Log.
claude --debug-file ./mod-debug.log --plugin-dir ./first-mod
tail -f ./mod-debug.log | grep first-mod

Ein geladener Mod gibt eine Zeile wie hooks module first-mod@inline loaded (worker, environment 2, tier user); events: session.start,tool.call,command.run,ui.render aus. Mit --plugin-dir geladene Mods tragen das Suffix @inline. Schreibe eigene Zeilen mit $.ui.log('message', { to: 'debug' }) — ohne das zweite Argument fügt $.ui.log stattdessen eine gedimmte Transkriptzeile hinzu. Meldungen, die man erkennen sollte: hook skipped: + Grund (Fehler geworfen, Timeout, falsche Form), reload failed, the previous version stays loaded: (die letzte funktionierende Version läuft weiter), ui.render (Pane) refused: (dein Baum hat nicht validiert — die Engine hat ihren eigenen gezeichnet) und … registered /tally but no command.run hook answered it.

Mods-Vokabular
Drücke Enter oder die Leertaste, um die Karte umzudrehen. Nutze die Pfeiltasten links und rechts, um zwischen den Karten zu wechseln.Begriff angezeigt.
1 / 8

Check yourself

0/5
  1. Dein tool.call-Hook blockiert riskante Bash-Befehle. Er wirft einen unerwarteten TypeError, bevor er next(e) erreicht. Was passiert mit dem Befehl?
  2. Warum führt `const ui = $.ui` dazu, dass ein Mod nicht geladen wird?
  3. Eine deny-Regel in deinen Settings verweigert Read(.env). Ein von dir installierter Mod ruft $.fs.read('.env') auf. Was passiert?
  4. Du willst, dass ein Pane, das dein Mod zeichnet, im Chat-Panel der VS-Code-Erweiterung sichtbar ist. Geht das?
  5. Welche Aussage über das 10-Sekunden-Hook-Budget ist korrekt?

Quellen & weiterführende Literatur​

Weiter​