diff --git a/CHANGELOG.md b/CHANGELOG.md index 75ee695..d629355 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,13 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), - _No unreleased entries yet._ +### Fixed + +- Fixed Claude Code hook integration (`gait-gate.sh`) to wrap hook responses in + the `hookSpecificOutput` envelope required by Claude Code's PreToolUse + protocol. Without this wrapper, Claude Code silently ignores hook responses, + making all gait verdicts (allow, deny, ask) unenforceable. + ### Changed - Gate intent normalization now treats omitted target `discovery_method` as `unknown` instead of empty so policies can deterministically match unknown/dynamic discovery paths. diff --git a/examples/integrations/claude_code/gait-gate.sh b/examples/integrations/claude_code/gait-gate.sh index 795bd7b..b5f73d6 100755 --- a/examples/integrations/claude_code/gait-gate.sh +++ b/examples/integrations/claude_code/gait-gate.sh @@ -16,14 +16,15 @@ emit_response() { import json import os -payload = { +inner = { + "hookEventName": "PreToolUse", "permissionDecision": os.environ["DECISION"], "permissionDecisionReason": os.environ["REASON"], } trace_path = os.environ.get("TRACE_PATH", "").strip() if trace_path: - payload["tracePath"] = trace_path -print(json.dumps(payload)) + inner["tracePath"] = trace_path +print(json.dumps({"hookSpecificOutput": inner})) PY } @@ -60,13 +61,14 @@ strict_mode = os.environ.get("STRICT_MODE", "0").strip().lower() in {"1", "true" trace_path = os.environ.get("TRACE_PATH", "").strip() def emit(decision: str, reason: str) -> None: - payload = { + inner = { + "hookEventName": "PreToolUse", "permissionDecision": decision, "permissionDecisionReason": reason, } if trace_path: - payload["tracePath"] = trace_path - print(json.dumps(payload)) + inner["tracePath"] = trace_path + print(json.dumps({"hookSpecificOutput": inner})) try: decoded = json.loads(proxy_output) if proxy_output.strip() else {}