Die Build-Phase nutzte Hooks als Leitplanken, die Aktionen ohne menschliche Beteiligung erlaubten oder blockierten (Stufe 3: Build). Ein Hook kann auch nachfragen und die Aktion pausieren, bis die Person, die die Sitzung ausführt, sie bestätigt. Wenn der Genehmigende jemand anderes ist, etwa ein Release-Manager, sucht der Hook nach dessen protokollierter Genehmigung und blockiert, bis diese vorliegt.
Dieser Play steht in Stufe 5: Deploy, weil das Release-Gate der klarste Fall ist, aber Hooks sind nicht deploy-spezifisch: Sie laufen überall dort, wo Claude handelt. Zum Beispiel können Hooks Änderungen an Migrationen und Infrastruktur ohne Change-Ticket während Stufe 3: Build blockieren und den Agenten davon abhalten, Testdateien während einer Fix-Aufgabe in Stufe 4: Test zu bearbeiten.
Erste Schritte
- Voraussetzungen: Keine.
- Infrastruktur: Eine schriftliche Liste der Genehmigungen, die der Änderungsprozess erfordert.
So setzt du es um
- Die Engineering-Leitung listet zusammen mit Change-Management und Compliance die menschlichen Genehmigungsgates auf, die erhalten bleiben müssen, etwa die Freigabe durch das Change-Management, die Release-Autorisierung und Änderungen an geschützten Pfaden.
- Der Platform-Engineer drückt jedes Gate als Hook aus, ein Skript, das vor der Aktion von Claude läuft und erlauben, nachfragen oder blockieren kann.
- Team-Hooks kommen in
.claude/settings.jsonin Git, und nicht verhandelbare Hooks kommen in die Managed Settings, die der Plattform- oder IT-Administration gehören, wo einzelne Engineers sie nicht abschalten können. - Eine Blockierung sollte sich selbst erklären, sodass, wenn ein Hook eine Aktion stoppt, der Grund und der Weg zur Genehmigung in Claudes Ausgabe erscheinen.
So sieht es aus
Dieses Beispiel ist das Kundenportal eines Versicherers, das in Dev, Staging und Produktion veröffentlicht wird. Das Deployment läuft über MCP-Tools, einen Server pro Umgebung, und jeder Server hat ein release-Tool.
Der folgende Eintrag ist ein eigenständiges Beispiel in der .claude/settings.json des Projekts. Sein Matcher erfasst release auf jedem Server. Die leere args-Liste bewirkt, dass Claude Code das Skript direkt ausführt, statt über eine Shell:
{
"hooks": {
"PreToolUse": [
{
"matcher": "mcp__deploy-.*__release",
"hooks": [
{ "type": "command",
"command": "${CLAUDE_PROJECT_DIR}/.claude/hooks/release-gate.sh",
"args": [] }
]
}
]
}
}Das Gate selbst, .claude/hooks/release-gate.sh:
#!/bin/bash
# Release-Gate. dev: erlauben. staging: nachfragen. production: ein genehmigtes Change-Ticket für diese Version, sonst blockieren.
input=$(cat)
tool=$(jq -r '.tool_name' <<<"$input")
version=$(jq -r '.tool_input.version' <<<"$input")
ticket=$(jq -r '.tool_input.ticket // empty' <<<"$input")
decide() { # decide <allow|ask|deny> <reason>
jq -n --arg decision "$1" --arg reason "$2" '{hookSpecificOutput: {
hookEventName: "PreToolUse",
permissionDecision: $decision,
permissionDecisionReason: $reason}}'
exit 0
}
case "$tool" in
mcp__deploy-dev__release)
decide allow "dev needs no approval" ;;
mcp__deploy-staging__release)
[ "$GITHUB_REF" = "refs/heads/main" ] && decide allow "pipeline release from main"
decide ask "This ships $version to staging. Approve only if you own this release." ;;
mcp__deploy-prod__release)
route="Open a change ticket at https://change.example.com/new for version $version, \
wait for the release manager to approve it, then call release again with that ticket number."
[[ "$ticket" =~ ^CHG-[0-9]+$ && "$version" =~ ^[0-9a-f]{7,40}$ ]] ||
decide deny "Production needs an approved change ticket. $route"
if approver=$(/usr/local/bin/change-ticket show --ticket "$ticket" --format json |
jq -er --arg v "$version" 'select(.state == "approved" and .version == $v) | .approver' 2>/dev/null); then
decide allow "$ticket approved by $approver"
fi
decide deny "$ticket is not approved for version $version. $route" ;;
*)
decide deny "No release rule for $tool. Add one to release-gate.sh first." ;;
esacIm Skript sind version und ticket die Eingaben, die Claude an das release-Tool übergibt. change-ticket steht für das eigene Kommandozeilen-Tool deines Change-Systems. Das Gate hängt davon ab, dass der Agent keine Möglichkeit hat, in die Datensätze zu schreiben, die er liest.
In einem nicht-interaktiven Lauf kann niemand ein ask beantworten, daher verweigert Claude Code den Aufruf. Deshalb erlaubt das Gate Staging aus der Pipeline auf main, wo der genehmigte Merge die Genehmigung darstellt.
Das Gate hat eine Grenze. Ein Engineer könnte GITHUB_REF manuell in seiner eigenen Sitzung setzen, obwohl das nur die Eingabeaufforderung überspringt, die er selbst beantwortet hätte.
Der Grund bei einem ask wird der Person angezeigt. Der Grund bei einem deny wird Claude angezeigt, sodass die Blockierungsmeldung den Weg zur Genehmigung nennt, den Claude weitergeben kann. Zuerst kommt das Staging-Ergebnis, dann Produktion ohne Ticket:
{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "ask",
"permissionDecisionReason": "This ships 4f2c9e1 to staging. Approve only if you own this release."
}
}{
"hookSpecificOutput": {
"hookEventName": "PreToolUse",
"permissionDecision": "deny",
"permissionDecisionReason": "Production needs an approved change ticket. Open a change ticket at https://change.example.com/new for version 4f2c9e1, wait for the release manager to approve it, then call release again with that ticket number."
}
}Governance-Überlegungen
Hooks sind die Genehmigungsgates. Die Gate-Bedingung wird jedes Mal für alle durchgesetzt. Erlaubnis- und Blockierungsentscheidungen werden mit einem Zeitstempel protokolliert. Das Gate definiert auch, was als Genehmigung zählt, sei es ein genehmigtes Change-Ticket oder die Freigabe des Release-Managers.
Managed Settings für ein reguliertes Unternehmen
Managed Settings für ein reguliertes Unternehmen, bereitgestellt vom Plattform-Team über Mobile Device Management (MDM) oder die Admin-Konsole. Engineers können keine der darin enthaltenen Einstellungen bearbeiten oder überschreiben. Siehe unten:
{
"permissions": {
"deny": [
"Read(.env*)", "Read(./secrets/**)",
"WebFetch", "Bash(curl *)", "Bash(wget *)"
],
"allow": [
"Bash(git *)", "Bash(make build)",
"Bash(make test)", "Bash(make lint)"
],
"disableBypassPermissionsMode": "disable"
},
"allowManagedPermissionRulesOnly": true,
"sandbox": {
"enabled": true,
"failIfUnavailable": true,
"allowUnsandboxedCommands": false,
"network": { "allowedDomains": ["git.internal.example.com", "registry.npmjs.org"] },
"credentials": {
"files": [
{ "path": "~/.ssh", "mode": "deny" },
{ "path": "~/.aws/credentials", "mode": "deny" }
],
"envVars": [ { "name": "GITHUB_TOKEN", "mode": "deny" } ]
}
},
"allowManagedHooksOnly": true,
"disableSideloadFlags": true,
"allowManagedMcpServersOnly": true,
"strictKnownMarketplaces": [
{ "source": "github", "repo": "example-corp/approved-plugins" }
],
"requiredMinimumVersion": "2.1.193"
}Was die Einstellungen in Kontrollbegriffen bewirken:
permissions.denyhält Secrets aus dem Kontext des Agenten heraus und blockiert beliebigen Netzwerk-Egress durch Tools.permissions.allowgenehmigt im Voraus die sichere innere Schleife, damit die Deny-Liste nicht zu Prompt-Müdigkeit führt.disableBypassPermissionsModezusammen mitallowManagedPermissionRulesOnlybedeutet, dass kein Engineer, keine Projektdatei und kein Kommandozeilen-Flag die Regeln erweitern kann.sandboxdeckt ab, was Berechtigungen nicht können. Ein Tool-Level-Deny auf WebFetch hindert einen Shell-Befehl nicht daran, das Netzwerk zu erreichen, während die Domain-Allowlist auf Betriebssystemebene den Egress vollständig blockiert, sodass die beiden ein Ziel auf unterschiedlichen Ebenen durchsetzen.failIfUnavailableundallowUnsandboxedCommandsmachen die Sandbox zu einer Vorbedingung, was bedeutet, dass Claude Code den Start verweigert, wenn die Sandbox nicht initialisiert werden kann, und ein Befehl, der innerhalb der Sandbox fehlschlägt, nicht außerhalb davon erneut versucht werden kann.credentialsbehandelt einen Fall, den die Deny-Regeln übersehen.permissions.denyregelt Claudes Datei-Tools, aber ein sandboxed Shell-Befehl könnte standardmäßig trotzdem~/.sshoder~/.aws/credentialslesen. Dieser Block verweigert diese Lesezugriffe und entfernt die aufgeführten Secrets aus der Umgebung von sandboxed Befehlen.allowManagedHooksOnlybedeutet, dass nur Hooks ausgeführt werden, die in Managed Settings definiert sind; Hooks in Benutzer-, Projekt- und lokalen Einstellungen werden blockiert, einschließlich des oben stehenden eigenständigen.claude/settings.json-Beispiels. Um das Genehmigungsgate dieses Plays durchgesetzt zu halten, definiere es im eigenenhooks-Block der Managed-Datei.disableSideloadFlagsundstrictKnownMarketplacesbedeuten, dass jeder Skill, Agent, Hook oder MCP-Server auf der Maschine eines Engineers über den von der Organisation genehmigten Plugin-Marketplace kam und nicht aus einem Home-Verzeichnis. Die Marketplace-Allowlist kontrolliert, was installiert werden kann, und die Flags, die ein Plugin, einen Agenten oder eine MCP-Konfiguration für einen einzelnen Lauf sideloaden würden, werden beim Start abgelehnt.allowManagedMcpServersOnlymacht die Tool-Oberfläche des Agenten zu einer Allowlist, die dem Plattform-Team gehört.requiredMinimumVersionverweigert den Start bei einer Version unterhalb der genehmigten Untergrenze, sodass die Kontrollen durch einen Build durchgesetzt werden, den die Organisation tatsächlich bewertet hat.
Betrachte das Beispiel als Ausgangspunkt, den du an deine eigene Umgebung anpassen kannst. Jede Deny-Regel entfernt eine Fähigkeit, und die richtige Balance hängt von der Datenklassifizierung des Repos ab. Die Settings-Referenz(öffnet in neuem Tab) dokumentiert alle Schlüssel, einschließlich der nur für Managed Settings verfügbaren.
So misst du es
Für die Hooks selbst:
- Frühindikator: Die Wartezeit an jedem Genehmigungsgate. Jede Hook-Entscheidung wird mit einem Zeitstempel und einem Erlaubnis- oder Blockierungsurteil in den OpenTelemetry-Export geschrieben, sodass die Wartezeit pro Gate sichtbar ist.
- Spätindikator: Gate-Verstöße, die vor und nach der Einführung der Hooks die Produktion erreichen, aus dem Incident-Tracker.