coderifts 4.0.0 → 4.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,37 @@
1
1
  # Changelog
2
2
 
3
+ ## 4.1.1
4
+
5
+ ### Fixed
6
+
7
+ - **P0-2: `mapDecisionSeverity` never maps missing `execution_action` to ALLOW.**
8
+ Missing EA in any body shape → `INDETERMINATE` (exit 2). Legacy decision→severity
9
+ map is allowed only when `decision_spec_version === "1.0"` on a non-v2 body
10
+ (no `decision_result`, no `preflight_mode`). Top-level-only `{decision:"ALLOW"}`
11
+ without that pin is not permission.
12
+ - **P0-3: parse-gap is fail-closed by default.** Unparseable stdin and missing
13
+ `tool_input.file_path` → **exit 2**, stderr
14
+ `unparseable input — refusing (fail-closed); set CODERIFTS_ADVISORY=1 to soften`,
15
+ JSONL `hook_blocked` with `cause: stdin_unparseable|missing_file_path`.
16
+ `CODERIFTS_ADVISORY=1` restores exit 0. API-key-before-parse order is unchanged
17
+ (garbage stdin + no key → `missing_api_key`). Non-spec Write stays exit 0.
18
+ Unknown-tool-on-spec was already exit 2.
19
+
20
+ ## 4.1.0
21
+
22
+ ### Added
23
+
24
+ - **`coderifts claude-hook` JSONL GuardEvent log.** When `CODERIFTS_GUARD_EVENT_LOG` is
25
+ set to an absolute file path, the hook appends one JSON object per line:
26
+ `preflight_start` / `preflight_result` / `preflight_unavailable` / `detection_skip`
27
+ plus terminal `hook_blocked` (exit 2) / `hook_continue` (exit 0) /
28
+ `hook_advisory_passthrough` (ADVISORY exit 0). Extra fields: `tool`, `path`,
29
+ `trigger_source:"claude_hook"`, `fingerprint` when the authorize envelope
30
+ returns one, `sessionId` (`CODERIFTS_SESSION_ID` or per-process). Unset env
31
+ → no file. Log write errors are swallowed and printed to stderr; **exit
32
+ codes are unchanged**. The hook does not claim `enforced` (no
33
+ receipt-verified execution path).
34
+
3
35
  ## 4.0.0
4
36
 
5
37
  **Breaking** — `coderifts claude-hook` is fail-closed by default. The CLI now
@@ -28,6 +60,15 @@ depends on `@coderifts/agent-guard` ^8.1.1.
28
60
  (never widen).
29
61
  - **`MIN_GUARD_MAJOR`:** 6 → 8 (`scripts/assert-guard-major.js`).
30
62
 
63
+ ### Added
64
+ - **`coderifts agent-setup` writes `.claude/settings.json`.** PreToolUse hook
65
+ (`matcher: Write|Edit|MultiEdit` → `coderifts claude-hook`, timeout 60)
66
+ beside the six rule files. Fail-closed default (P0-3/P0-4). Explicit
67
+ opt-out documented on stdout: `CODERIFTS_ADVISORY=1`. `--check` is
68
+ byte-stable against the embed (same drift-gate as the six rule files).
69
+ Existing `.claude/settings.json` is skipped (no merge); `--force`
70
+ replaces the **entire** file.
71
+
31
72
  ### Fixed
32
73
  - **Installer copy (P0 #9 / e76e846):** `agent-setup` / `init` teach
33
74
  guard@8 execution-state **DEFAULT ON**, not the old OFF.
package/README.md CHANGED
@@ -301,7 +301,8 @@ coderifts enforce --repo owner/repo --apply
301
301
 
302
302
  ### Agent host files (`coderifts agent-setup`)
303
303
 
304
- Writes the six CodeRifts agent-host rule files into the current repo (or `--out <dir>`):
304
+ Writes the six CodeRifts agent-host rule files **plus** the Claude Code
305
+ PreToolUse hook settings into the current repo (or `--out <dir>`):
305
306
 
306
307
  | File | Host |
307
308
  |------|------|
@@ -311,6 +312,16 @@ Writes the six CodeRifts agent-host rule files into the current repo (or `--out
311
312
  | `.github/copilot-instructions.md` | GitHub Copilot |
312
313
  | `coderifts-langgraph-policy.js` | LangGraph |
313
314
  | `openai-agent-instructions.md` | OpenAI Agents SDK |
315
+ | `.claude/settings.json` | Claude Code PreToolUse → `coderifts claude-hook` (fail-closed) |
316
+
317
+ The generated `.claude/settings.json` matches the recipe above (`matcher`
318
+ `Write|Edit|MultiEdit`, `command` `coderifts claude-hook`, `timeout` 60).
319
+ Default is **fail-closed** (missing key / API down → exit 2). Explicit
320
+ opt-out: `CODERIFTS_ADVISORY=1|true`. `CODERIFTS_STRICT=1` cannot be
321
+ weakened by ADVISORY. Existing files are skipped unless `--force`.
322
+ `--force` **replaces the entire** `.claude/settings.json` (no merge into
323
+ permissions / other hooks). If the file already exists, add the PreToolUse
324
+ recipe by hand or accept a full replace.
314
325
 
315
326
  ```bash
316
327
  coderifts agent-setup # write (skip existing files)
package/dist/cli.js CHANGED
@@ -3007,7 +3007,7 @@ var require_package = __commonJS({
3007
3007
  "package.json"(exports2, module2) {
3008
3008
  module2.exports = {
3009
3009
  name: "coderifts",
3010
- version: "4.0.0",
3010
+ version: "4.1.1",
3011
3011
  description: "Detect breaking API changes from the command line. Works locally or with the CodeRifts cloud API.",
3012
3012
  author: "CodeRifts <hello@coderifts.com>",
3013
3013
  license: "MIT",
@@ -100059,7 +100059,8 @@ var require_agent_host_files_embedded = __commonJS({
100059
100059
  ".cursor/rules/coderifts.mdc",
100060
100060
  ".github/copilot-instructions.md",
100061
100061
  "coderifts-langgraph-policy.js",
100062
- "openai-agent-instructions.md"
100062
+ "openai-agent-instructions.md",
100063
+ ".claude/settings.json"
100063
100064
  ]);
100064
100065
  var AGENT_HOST_FILES = Object.freeze({
100065
100066
  "AGENTS.md": '<!-- GENERATED from CodeRifts agent-host-rule (single source). Do not edit rule sentences by hand; run: node scripts/generate-agent-host-files.js -->\n\n# CodeRifts agent rules\n\nCall `preflight_change_set` before merge, deploy, publish, or tool registration when a contract artifact changed (OpenAPI/Swagger, GraphQL, gRPC/protobuf, AsyncAPI, MCP manifest, or agent tool schemas).\n\nSend the complete base-to-head change set: every changed contract artifact with full before and after content. Do not send a single-file subset when other contract files also change.\n\nBranch on `execution_action` only. Do not branch on `decision` and do not branch on `safe_for_agent` (not_for_control_flow_use_execution_action). Canonical `execution_action` values: CONTINUE, CONTINUE_WITH_MONITORING, REQUEST_APPROVAL, STOP.\n\nAn unrecognised `execution_action` is not permission: fail closed (halt or re-preflight). Well-known code: `not_permission_fail_closed`.\n\n`CONTINUE_WITH_MONITORING` requires a wired monitoring sink (`monitoringSinkWired`). It is not "proceed with caution" without monitoring.\n\nDo not call CodeRifts tools for a documentation-only change (README, guides, comments) with no contract artifact content change.\n\nIf you already hold a chain receipt and only need authenticity/lifecycle: `verify_receipt`. If you need a past decision by id: `get_decision_details`. Neither replaces preflight for a new change set.\n\nA receipt is operation-scoped: a merge receipt never authorizes a deploy or publish. When the operation changes, you MUST call `preflight_change_set` again with the new `context.operation` \u2014 do not decline, re-preflight.\n\nA stale or superseded receipt on a changed head requires a NEW preflight \u2014 `verify_receipt` cannot re-diff.\n\nFor mutating tools, put only the guarded version in the agent\'s tool table; keep the raw handler host-only and unreachable from that table. How you name tools is yours \u2014 this is a reachability property, not a product rename of host tools. CodeRifts cannot see or stop a raw call the host makes outside the table it returns; adopt this as a host convention, not as a guarantee from the package.\n\nCodeRifts reports a governance decision and `execution_action`; it does not by itself block merges. Blocking requires separate repository configuration (required status checks, enforcement) that this rule file does not set.\n',
@@ -100067,7 +100068,8 @@ var require_agent_host_files_embedded = __commonJS({
100067
100068
  ".cursor/rules/coderifts.mdc": '---\ndescription: CodeRifts API governance \u2014 when to preflight and how to branch\nglobs:\nalwaysApply: true\n---\n\n<!-- GENERATED from CodeRifts agent-host-rule (single source). Do not edit rule sentences by hand; run: node scripts/generate-agent-host-files.js -->\n\n# CodeRifts\n\nCall `preflight_change_set` before merge, deploy, publish, or tool registration when a contract artifact changed (OpenAPI/Swagger, GraphQL, gRPC/protobuf, AsyncAPI, MCP manifest, or agent tool schemas).\n\nSend the complete base-to-head change set: every changed contract artifact with full before and after content. Do not send a single-file subset when other contract files also change.\n\nBranch on `execution_action` only. Do not branch on `decision` and do not branch on `safe_for_agent` (not_for_control_flow_use_execution_action). Canonical `execution_action` values: CONTINUE, CONTINUE_WITH_MONITORING, REQUEST_APPROVAL, STOP.\n\nAn unrecognised `execution_action` is not permission: fail closed (halt or re-preflight). Well-known code: `not_permission_fail_closed`.\n\n`CONTINUE_WITH_MONITORING` requires a wired monitoring sink (`monitoringSinkWired`). It is not "proceed with caution" without monitoring.\n\nDo not call CodeRifts tools for a documentation-only change (README, guides, comments) with no contract artifact content change.\n\nIf you already hold a chain receipt and only need authenticity/lifecycle: `verify_receipt`. If you need a past decision by id: `get_decision_details`. Neither replaces preflight for a new change set.\n\nA receipt is operation-scoped: a merge receipt never authorizes a deploy or publish. When the operation changes, you MUST call `preflight_change_set` again with the new `context.operation` \u2014 do not decline, re-preflight.\n\nA stale or superseded receipt on a changed head requires a NEW preflight \u2014 `verify_receipt` cannot re-diff.\n\nFor mutating tools, put only the guarded version in the agent\'s tool table; keep the raw handler host-only and unreachable from that table. How you name tools is yours \u2014 this is a reachability property, not a product rename of host tools. CodeRifts cannot see or stop a raw call the host makes outside the table it returns; adopt this as a host convention, not as a guarantee from the package.\n\nCodeRifts reports a governance decision and `execution_action`; it does not by itself block merges. Blocking requires separate repository configuration (required status checks, enforcement) that this rule file does not set.\n',
100068
100069
  ".github/copilot-instructions.md": '<!-- GENERATED from CodeRifts agent-host-rule (single source). Do not edit rule sentences by hand; run: node scripts/generate-agent-host-files.js -->\n\n# CodeRifts instructions for GitHub Copilot\n\nCall `preflight_change_set` before merge, deploy, publish, or tool registration when a contract artifact changed (OpenAPI/Swagger, GraphQL, gRPC/protobuf, AsyncAPI, MCP manifest, or agent tool schemas).\n\nSend the complete base-to-head change set: every changed contract artifact with full before and after content. Do not send a single-file subset when other contract files also change.\n\nBranch on `execution_action` only. Do not branch on `decision` and do not branch on `safe_for_agent` (not_for_control_flow_use_execution_action). Canonical `execution_action` values: CONTINUE, CONTINUE_WITH_MONITORING, REQUEST_APPROVAL, STOP.\n\nAn unrecognised `execution_action` is not permission: fail closed (halt or re-preflight). Well-known code: `not_permission_fail_closed`.\n\n`CONTINUE_WITH_MONITORING` requires a wired monitoring sink (`monitoringSinkWired`). It is not "proceed with caution" without monitoring.\n\nDo not call CodeRifts tools for a documentation-only change (README, guides, comments) with no contract artifact content change.\n\nIf you already hold a chain receipt and only need authenticity/lifecycle: `verify_receipt`. If you need a past decision by id: `get_decision_details`. Neither replaces preflight for a new change set.\n\nA receipt is operation-scoped: a merge receipt never authorizes a deploy or publish. When the operation changes, you MUST call `preflight_change_set` again with the new `context.operation` \u2014 do not decline, re-preflight.\n\nA stale or superseded receipt on a changed head requires a NEW preflight \u2014 `verify_receipt` cannot re-diff.\n\nFor mutating tools, put only the guarded version in the agent\'s tool table; keep the raw handler host-only and unreachable from that table. How you name tools is yours \u2014 this is a reachability property, not a product rename of host tools. CodeRifts cannot see or stop a raw call the host makes outside the table it returns; adopt this as a host convention, not as a guarantee from the package.\n\nCodeRifts reports a governance decision and `execution_action`; it does not by itself block merges. Blocking requires separate repository configuration (required status checks, enforcement) that this rule file does not set.\n',
100069
100070
  "coderifts-langgraph-policy.js": '// GENERATED from CodeRifts agent-host-rule (single source). Do not edit rule sentences by hand; run: node scripts/generate-agent-host-files.js\n// System policy string for a LangGraph (or similar) agent. Content is generated;\n// identical rule sentences to AGENTS.md / other formats.\n\'use strict\';\n\nmodule.exports = "Call `preflight_change_set` before merge, deploy, publish, or tool registration when a contract artifact changed (OpenAPI/Swagger, GraphQL, gRPC/protobuf, AsyncAPI, MCP manifest, or agent tool schemas).\\n\\nSend the complete base-to-head change set: every changed contract artifact with full before and after content. Do not send a single-file subset when other contract files also change.\\n\\nBranch on `execution_action` only. Do not branch on `decision` and do not branch on `safe_for_agent` (not_for_control_flow_use_execution_action). Canonical `execution_action` values: CONTINUE, CONTINUE_WITH_MONITORING, REQUEST_APPROVAL, STOP.\\n\\nAn unrecognised `execution_action` is not permission: fail closed (halt or re-preflight). Well-known code: `not_permission_fail_closed`.\\n\\n`CONTINUE_WITH_MONITORING` requires a wired monitoring sink (`monitoringSinkWired`). It is not \\"proceed with caution\\" without monitoring.\\n\\nDo not call CodeRifts tools for a documentation-only change (README, guides, comments) with no contract artifact content change.\\n\\nIf you already hold a chain receipt and only need authenticity/lifecycle: `verify_receipt`. If you need a past decision by id: `get_decision_details`. Neither replaces preflight for a new change set.\\n\\nA receipt is operation-scoped: a merge receipt never authorizes a deploy or publish. When the operation changes, you MUST call `preflight_change_set` again with the new `context.operation` \u2014 do not decline, re-preflight.\\n\\nA stale or superseded receipt on a changed head requires a NEW preflight \u2014 `verify_receipt` cannot re-diff.\\n\\nFor mutating tools, put only the guarded version in the agent\'s tool table; keep the raw handler host-only and unreachable from that table. How you name tools is yours \u2014 this is a reachability property, not a product rename of host tools. CodeRifts cannot see or stop a raw call the host makes outside the table it returns; adopt this as a host convention, not as a guarantee from the package.\\n\\nCodeRifts reports a governance decision and `execution_action`; it does not by itself block merges. Blocking requires separate repository configuration (required status checks, enforcement) that this rule file does not set.";\n',
100070
- "openai-agent-instructions.md": '<!-- GENERATED from CodeRifts agent-host-rule (single source). Do not edit rule sentences by hand; run: node scripts/generate-agent-host-files.js -->\n\n# CodeRifts agent instructions\n\nCall `preflight_change_set` before merge, deploy, publish, or tool registration when a contract artifact changed (OpenAPI/Swagger, GraphQL, gRPC/protobuf, AsyncAPI, MCP manifest, or agent tool schemas).\n\nSend the complete base-to-head change set: every changed contract artifact with full before and after content. Do not send a single-file subset when other contract files also change.\n\nBranch on `execution_action` only. Do not branch on `decision` and do not branch on `safe_for_agent` (not_for_control_flow_use_execution_action). Canonical `execution_action` values: CONTINUE, CONTINUE_WITH_MONITORING, REQUEST_APPROVAL, STOP.\n\nAn unrecognised `execution_action` is not permission: fail closed (halt or re-preflight). Well-known code: `not_permission_fail_closed`.\n\n`CONTINUE_WITH_MONITORING` requires a wired monitoring sink (`monitoringSinkWired`). It is not "proceed with caution" without monitoring.\n\nDo not call CodeRifts tools for a documentation-only change (README, guides, comments) with no contract artifact content change.\n\nIf you already hold a chain receipt and only need authenticity/lifecycle: `verify_receipt`. If you need a past decision by id: `get_decision_details`. Neither replaces preflight for a new change set.\n\nA receipt is operation-scoped: a merge receipt never authorizes a deploy or publish. When the operation changes, you MUST call `preflight_change_set` again with the new `context.operation` \u2014 do not decline, re-preflight.\n\nA stale or superseded receipt on a changed head requires a NEW preflight \u2014 `verify_receipt` cannot re-diff.\n\nFor mutating tools, put only the guarded version in the agent\'s tool table; keep the raw handler host-only and unreachable from that table. How you name tools is yours \u2014 this is a reachability property, not a product rename of host tools. CodeRifts cannot see or stop a raw call the host makes outside the table it returns; adopt this as a host convention, not as a guarantee from the package.\n\nCodeRifts reports a governance decision and `execution_action`; it does not by itself block merges. Blocking requires separate repository configuration (required status checks, enforcement) that this rule file does not set.\n'
100071
+ "openai-agent-instructions.md": '<!-- GENERATED from CodeRifts agent-host-rule (single source). Do not edit rule sentences by hand; run: node scripts/generate-agent-host-files.js -->\n\n# CodeRifts agent instructions\n\nCall `preflight_change_set` before merge, deploy, publish, or tool registration when a contract artifact changed (OpenAPI/Swagger, GraphQL, gRPC/protobuf, AsyncAPI, MCP manifest, or agent tool schemas).\n\nSend the complete base-to-head change set: every changed contract artifact with full before and after content. Do not send a single-file subset when other contract files also change.\n\nBranch on `execution_action` only. Do not branch on `decision` and do not branch on `safe_for_agent` (not_for_control_flow_use_execution_action). Canonical `execution_action` values: CONTINUE, CONTINUE_WITH_MONITORING, REQUEST_APPROVAL, STOP.\n\nAn unrecognised `execution_action` is not permission: fail closed (halt or re-preflight). Well-known code: `not_permission_fail_closed`.\n\n`CONTINUE_WITH_MONITORING` requires a wired monitoring sink (`monitoringSinkWired`). It is not "proceed with caution" without monitoring.\n\nDo not call CodeRifts tools for a documentation-only change (README, guides, comments) with no contract artifact content change.\n\nIf you already hold a chain receipt and only need authenticity/lifecycle: `verify_receipt`. If you need a past decision by id: `get_decision_details`. Neither replaces preflight for a new change set.\n\nA receipt is operation-scoped: a merge receipt never authorizes a deploy or publish. When the operation changes, you MUST call `preflight_change_set` again with the new `context.operation` \u2014 do not decline, re-preflight.\n\nA stale or superseded receipt on a changed head requires a NEW preflight \u2014 `verify_receipt` cannot re-diff.\n\nFor mutating tools, put only the guarded version in the agent\'s tool table; keep the raw handler host-only and unreachable from that table. How you name tools is yours \u2014 this is a reachability property, not a product rename of host tools. CodeRifts cannot see or stop a raw call the host makes outside the table it returns; adopt this as a host convention, not as a guarantee from the package.\n\nCodeRifts reports a governance decision and `execution_action`; it does not by itself block merges. Blocking requires separate repository configuration (required status checks, enforcement) that this rule file does not set.\n',
100072
+ ".claude/settings.json": '{\n "hooks": {\n "PreToolUse": [\n {\n "matcher": "Write|Edit|MultiEdit",\n "hooks": [\n {\n "type": "command",\n "command": "coderifts claude-hook",\n "timeout": 60\n }\n ]\n }\n ]\n }\n}\n'
100071
100073
  });
100072
100074
  module2.exports = { AGENT_HOST_FILES, AGENT_HOST_PATHS };
100073
100075
  }
@@ -100207,6 +100209,15 @@ ${USAGE}` };
100207
100209
  for (const line of AGENT_SETUP_WARN_TELEMETRY_LINES) {
100208
100210
  log(line);
100209
100211
  }
100212
+ if (summary.skipped.includes(".claude/settings.json")) {
100213
+ for (const line of AGENT_SETUP_HOOK_SKIP_LINES) {
100214
+ log(line);
100215
+ }
100216
+ } else if (summary.written.includes(".claude/settings.json")) {
100217
+ for (const line of AGENT_SETUP_HOOK_WIRE_LINES) {
100218
+ log(line);
100219
+ }
100220
+ }
100210
100221
  }
100211
100222
  if (doExit) process.exit(0);
100212
100223
  return { exitCode: 0, code: "OK", outDir, ...summary };
@@ -100222,13 +100233,28 @@ ${USAGE}` };
100222
100233
  chalk.dim(" \xB7 missing artifacts/fingerprint \u2192 execution_state_unmeasurable (quiet)"),
100223
100234
  chalk.dim(" Merge gate block is separate: coderifts setup-required-check --apply")
100224
100235
  ]);
100236
+ var AGENT_SETUP_HOOK_WIRE_LINES = Object.freeze([
100237
+ chalk.bold(" Claude Code hook (.claude/settings.json): fail-closed PreToolUse"),
100238
+ chalk.dim(" matcher Write|Edit|MultiEdit \u2192 coderifts claude-hook (exit 2 = BLOCK)."),
100239
+ chalk.dim(" Missing key / API down / unreadable spec \u2192 exit 2 (enforce_indeterminate), not ALLOW."),
100240
+ chalk.dim(" Opt-out: CODERIFTS_ADVISORY=1|true restores soft-allow when governance could not run."),
100241
+ chalk.dim(" CODERIFTS_STRICT=1 cannot be weakened by ADVISORY.")
100242
+ ]);
100243
+ var AGENT_SETUP_HOOK_SKIP_LINES = Object.freeze([
100244
+ chalk.yellow(" .claude/settings.json exists \u2014 left unchanged (installer does not merge)."),
100245
+ chalk.dim(" --force replaces the ENTIRE file (not just the PreToolUse hook)."),
100246
+ chalk.dim(" To wire fail-closed: add PreToolUse \u2192 coderifts claude-hook by hand, or --force."),
100247
+ chalk.dim(" Opt-out if wired: CODERIFTS_ADVISORY=1|true. CODERIFTS_STRICT cannot weaken.")
100248
+ ]);
100225
100249
  module2.exports = {
100226
100250
  runAgentSetup,
100227
100251
  parseAgentSetupArgs,
100228
100252
  AGENT_HOST_FILES,
100229
100253
  AGENT_HOST_PATHS,
100230
100254
  USAGE,
100231
- AGENT_SETUP_WARN_TELEMETRY_LINES
100255
+ AGENT_SETUP_WARN_TELEMETRY_LINES,
100256
+ AGENT_SETUP_HOOK_WIRE_LINES,
100257
+ AGENT_SETUP_HOOK_SKIP_LINES
100232
100258
  };
100233
100259
  }
100234
100260
  });
@@ -101205,8 +101231,10 @@ var require_claude_hook = __commonJS({
101205
101231
  " no API key / API unreachable / disk unreadable / edit-apply failure on the spec path",
101206
101232
  " Absence of a key is not permission. Explicit opt-out: CODERIFTS_ADVISORY=1|true.",
101207
101233
  "",
101208
- "Still exit 0 (nothing to govern): unparseable stdin, missing file_path, non-spec path,",
101209
- "identical content.",
101234
+ "Parse-gap DEFAULT (exit 2): unparseable stdin / missing file_path \u2014 refusing (fail-closed);",
101235
+ " set CODERIFTS_ADVISORY=1 to soften. JSONL hook_blocked cause stdin_unparseable|missing_file_path.",
101236
+ "",
101237
+ "Still exit 0 (nothing to govern): non-spec path, identical content.",
101210
101238
  "",
101211
101239
  "CONTINUE_WITH_MONITORING: allow only if the host asserts a sink",
101212
101240
  " (CODERIFTS_MONITORING_SINK_WIRED=1|true or git config coderifts.monitoringSinkWired).",
@@ -101248,6 +101276,33 @@ var require_claude_hook = __commonJS({
101248
101276
  if (dr && typeof dr === "object" && dr.monitoringSinkWired === true) return true;
101249
101277
  return false;
101250
101278
  }
101279
+ var HOOK_TRIGGER_SOURCE = "claude_hook";
101280
+ function extractFingerprint(result) {
101281
+ const d = result && typeof result === "object" && !Array.isArray(result) ? result : {};
101282
+ const dr = d.decision_result && typeof d.decision_result === "object" ? d.decision_result : {};
101283
+ const fp = dr.fingerprint || d.fingerprint || d.verdict_fingerprint || d.input_fingerprint;
101284
+ return typeof fp === "string" && fp.length > 0 ? fp : void 0;
101285
+ }
101286
+ function hookSessionId(env) {
101287
+ const fromEnv = env && env.CODERIFTS_SESSION_ID && String(env.CODERIFTS_SESSION_ID).trim();
101288
+ if (fromEnv) return fromEnv;
101289
+ if (!hookSessionId._gen) hookSessionId._gen = `hook-${process.pid}-${Date.now()}`;
101290
+ return hookSessionId._gen;
101291
+ }
101292
+ function appendGuardEventLog(env, event, deps = {}) {
101293
+ const p = env && env.CODERIFTS_GUARD_EVENT_LOG;
101294
+ if (p == null || String(p).trim() === "") return;
101295
+ const errLog = deps.errLog || ((m) => console.error(String(m)));
101296
+ const append = deps.appendFileSync || ((file, data) => fs.appendFileSync(file, data));
101297
+ try {
101298
+ append(String(p).trim(), JSON.stringify(event) + "\n");
101299
+ } catch (err) {
101300
+ try {
101301
+ errLog(`CodeRifts claude-hook: CODERIFTS_GUARD_EVENT_LOG write failed (${err && err.message})`);
101302
+ } catch {
101303
+ }
101304
+ }
101305
+ }
101251
101306
  function failClosedOrAdvisory({ advisory, strict, site, softMsg, why, errLog }) {
101252
101307
  if (advisory && !strict) {
101253
101308
  errLog(`${softMsg} (CODERIFTS_ADVISORY)`);
@@ -101378,13 +101433,22 @@ var require_claude_hook = __commonJS({
101378
101433
  }
101379
101434
  return { ok: false, reason: `unsupported tool_name for content derive: ${name}` };
101380
101435
  }
101436
+ function isV2DecisionBody(d, dr) {
101437
+ if (dr && typeof dr === "object") return true;
101438
+ if (d.preflight_mode != null && d.preflight_mode !== "") return true;
101439
+ const ver = d.decision_spec_version;
101440
+ return typeof ver === "string" && ver.startsWith("2.");
101441
+ }
101442
+ function allowLegacyDecisionMap(d, dr) {
101443
+ return d.decision_spec_version === "1.0" && !isV2DecisionBody(d, dr);
101444
+ }
101381
101445
  function mapDecisionSeverity(result) {
101382
101446
  const d = result && typeof result === "object" && !Array.isArray(result) ? result : {};
101383
101447
  let ea = null;
101384
101448
  const dr = d.decision_result;
101385
- if (dr && typeof dr === "object" && typeof dr.execution_action === "string") {
101449
+ if (dr && typeof dr === "object" && typeof dr.execution_action === "string" && dr.execution_action !== "") {
101386
101450
  ea = dr.execution_action;
101387
- } else if (typeof d.execution_action === "string") {
101451
+ } else if (typeof d.execution_action === "string" && d.execution_action !== "") {
101388
101452
  ea = d.execution_action;
101389
101453
  }
101390
101454
  const hasDecision = d.omega_decision != null && d.omega_decision !== "" || d.decision != null && d.decision !== "" || dr && typeof dr === "object" && dr.execution_action;
@@ -101409,7 +101473,7 @@ var require_claude_hook = __commonJS({
101409
101473
  } else {
101410
101474
  severity = "REQUIRE_APPROVAL";
101411
101475
  }
101412
- } else {
101476
+ } else if (allowLegacyDecisionMap(d, dr)) {
101413
101477
  const od = d.omega_decision || d.decision;
101414
101478
  if (od == null || od === "") {
101415
101479
  severity = "INDETERMINATE";
@@ -101417,6 +101481,8 @@ var require_claude_hook = __commonJS({
101417
101481
  else if (od === "REQUIRE_APPROVAL") severity = "REQUIRE_APPROVAL";
101418
101482
  else if (od === "WARN") severity = "WARN";
101419
101483
  else severity = "ALLOW";
101484
+ } else {
101485
+ severity = "INDETERMINATE";
101420
101486
  }
101421
101487
  const decision = dr && dr.decision || d.decision || d.omega_decision || null;
101422
101488
  const decisionId = dr && dr.decision_id || d.decision_id || null;
@@ -101427,6 +101493,45 @@ var require_claude_hook = __commonJS({
101427
101493
  executionAction: ea
101428
101494
  };
101429
101495
  }
101496
+ function renderDecisionWhy(result, opts = {}) {
101497
+ const maxFixes = Number.isInteger(opts.maxFixes) ? opts.maxFixes : 5;
101498
+ const maxLines = Number.isInteger(opts.maxLines) ? opts.maxLines : 13;
101499
+ const d = result && typeof result === "object" && !Array.isArray(result) ? result : {};
101500
+ const dr = d.decision_result && typeof d.decision_result === "object" ? d.decision_result : {};
101501
+ const pick = (k) => dr[k] !== void 0 ? dr[k] : d[k];
101502
+ const arr = (v) => Array.isArray(v) ? v : [];
101503
+ const str = (v) => typeof v === "string" ? v.trim() : "";
101504
+ const lines = [];
101505
+ const reasonCodes = [];
101506
+ for (const r of arr(pick("blocking_reasons"))) {
101507
+ if (!r || typeof r !== "object") continue;
101508
+ const code = str(r.code);
101509
+ const message = str(r.message);
101510
+ if (code) reasonCodes.push(code);
101511
+ if (!code && !message) continue;
101512
+ lines.push(`- ${code || "REASON"}${message ? `: ${message}` : ""}`);
101513
+ }
101514
+ for (const r of arr(pick("degraded_reasons"))) {
101515
+ if (!r || typeof r !== "object") continue;
101516
+ const code = str(r.code);
101517
+ const message = str(r.message);
101518
+ if (!code && !message) continue;
101519
+ lines.push(`- degraded: ${[code, message].filter(Boolean).join(": ")}`);
101520
+ }
101521
+ const action = str(pick("required_action"));
101522
+ if (action) lines.push(`- action: ${action}`);
101523
+ const rt = pick("remediation_transaction");
101524
+ const changes = arr(rt && typeof rt === "object" ? rt.required_changes : null).filter((c) => c && typeof c === "object");
101525
+ const shown = changes.slice(0, maxFixes);
101526
+ for (const c of shown) {
101527
+ const target = str(c.target);
101528
+ const instruction = str(c.instruction);
101529
+ if (!instruction && !target) continue;
101530
+ lines.push(`- fix${target ? ` (${target})` : ""}: ${instruction || str(c.precise_label)}`);
101531
+ }
101532
+ if (changes.length > shown.length) lines.push(`- fix: +${changes.length - shown.length} more`);
101533
+ return { lines: lines.slice(0, maxLines), reasonCodes };
101534
+ }
101430
101535
  async function runClaudeHook(options = {}, deps = {}) {
101431
101536
  const errLog = deps.errLog || ((m) => console.error(String(m)));
101432
101537
  const readStdin = deps.readStdin || (() => {
@@ -101443,32 +101548,79 @@ var require_claude_hook = __commonJS({
101443
101548
  const env = deps.env || process.env;
101444
101549
  const strict = isStrictMode(env);
101445
101550
  const advisory = isAdvisoryMode(env);
101551
+ const sessionId = hookSessionId(env);
101552
+ const logEv = (partial) => {
101553
+ appendGuardEventLog(env, {
101554
+ at: (/* @__PURE__ */ new Date()).toISOString(),
101555
+ sessionId,
101556
+ trigger_source: HOOK_TRIGGER_SOURCE,
101557
+ ...partial
101558
+ }, { errLog, appendFileSync: deps.appendFileSync });
101559
+ };
101560
+ let tool = null;
101561
+ let filePathKnown = null;
101562
+ const emitTerminal = (r) => {
101563
+ const extra = {
101564
+ tool: tool || void 0,
101565
+ path: filePathKnown || void 0,
101566
+ decision: r.decision != null ? r.decision : void 0,
101567
+ decisionId: r.decisionId != null ? r.decisionId : void 0
101568
+ };
101569
+ if (r.exitCode === 2) {
101570
+ const reasons = Array.isArray(r.reasons) && r.reasons.length ? r.reasons : void 0;
101571
+ logEv({
101572
+ type: "hook_blocked",
101573
+ exit: 2,
101574
+ ...extra,
101575
+ reasons,
101576
+ cause: r.reason || r.site
101577
+ });
101578
+ } else if (r.exitCode === 0 && r.site && advisory && !strict) {
101579
+ logEv({ type: "hook_advisory_passthrough", exit: 0, decision: r.decision || null, ...extra, cause: r.site });
101580
+ } else {
101581
+ logEv({ type: "hook_continue", exit: 0, ...extra, cause: r.reason });
101582
+ }
101583
+ return r;
101584
+ };
101446
101585
  const apiKey = resolveApiKey({ ...deps, cwd, env });
101447
101586
  if (!apiKey) {
101448
- return failClosedOrAdvisory({
101587
+ logEv({ type: "preflight_unavailable", cause: "missing_api_key" });
101588
+ return emitTerminal(failClosedOrAdvisory({
101449
101589
  advisory,
101450
101590
  strict,
101451
101591
  site: "missing_api_key",
101452
101592
  why: "no API key",
101453
101593
  softMsg: "CodeRifts claude-hook: no API key (coderifts login / CODERIFTS_API_KEY / git config coderifts.apiKey) \u2014 allowing",
101454
101594
  errLog
101455
- });
101595
+ }));
101456
101596
  }
101597
+ const PARSE_GAP_STDERR = "unparseable input \u2014 refusing (fail-closed); set CODERIFTS_ADVISORY=1 to soften";
101457
101598
  const raw = typeof options.stdin === "string" ? options.stdin : readStdin();
101458
101599
  const parsed = parseStdinJson(raw);
101459
101600
  if (!parsed.ok) {
101460
- errLog(`CodeRifts claude-hook: ${parsed.reason} \u2014 allowing (soft; never block on parse gap)`);
101461
- return { exitCode: 0, reason: "stdin_unparseable" };
101601
+ if (advisory && !strict) {
101602
+ errLog(`CodeRifts claude-hook: ${parsed.reason} \u2014 allowing (CODERIFTS_ADVISORY)`);
101603
+ return emitTerminal({ exitCode: 0, reason: "stdin_unparseable", site: "stdin_unparseable" });
101604
+ }
101605
+ errLog(`CodeRifts claude-hook: ${PARSE_GAP_STDERR}`);
101606
+ return emitTerminal({ exitCode: 2, reason: "stdin_unparseable" });
101462
101607
  }
101463
101608
  const { toolName, toolInput } = parsed;
101609
+ tool = toolName;
101464
101610
  const filePath = toolInput.file_path || toolInput.filePath || toolInput.path;
101465
101611
  if (!filePath || typeof filePath !== "string") {
101466
- errLog("CodeRifts claude-hook: tool_input.file_path missing \u2014 allowing (soft)");
101467
- return { exitCode: 0, reason: "missing_file_path" };
101612
+ if (advisory && !strict) {
101613
+ errLog("CodeRifts claude-hook: tool_input.file_path missing \u2014 allowing (CODERIFTS_ADVISORY)");
101614
+ return emitTerminal({ exitCode: 0, reason: "missing_file_path", site: "missing_file_path" });
101615
+ }
101616
+ errLog(`CodeRifts claude-hook: ${PARSE_GAP_STDERR}`);
101617
+ return emitTerminal({ exitCode: 2, reason: "missing_file_path" });
101468
101618
  }
101619
+ filePathKnown = filePath;
101469
101620
  const specPath = resolveSpecPath({ ...deps, cwd });
101470
101621
  if (!isSpecPath(filePath, specPath)) {
101471
- return { exitCode: 0, reason: "not_spec_path" };
101622
+ logEv({ type: "detection_skip", signals: ["not_spec_path"], tool, path: filePath });
101623
+ return emitTerminal({ exitCode: 0, reason: "not_spec_path" });
101472
101624
  }
101473
101625
  const absPath = path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath);
101474
101626
  let diskBefore = "";
@@ -101476,30 +101628,34 @@ var require_claude_hook = __commonJS({
101476
101628
  try {
101477
101629
  diskBefore = readFile(absPath, "utf8");
101478
101630
  } catch (e) {
101479
- return failClosedOrAdvisory({
101631
+ logEv({ type: "preflight_unavailable", cause: "disk_unreadable", tool, path: filePath });
101632
+ return emitTerminal(failClosedOrAdvisory({
101480
101633
  advisory,
101481
101634
  strict,
101482
101635
  site: "disk_unreadable",
101483
101636
  why: `cannot read contract file ${absPath}`,
101484
101637
  softMsg: `CodeRifts claude-hook: cannot read ${absPath} \u2014 allowing (soft)`,
101485
101638
  errLog
101486
- });
101639
+ }));
101487
101640
  }
101488
101641
  }
101489
101642
  const derived = deriveAfterContent(toolName, toolInput, diskBefore);
101490
101643
  if (!derived.ok) {
101491
- return failClosedOrAdvisory({
101644
+ logEv({ type: "preflight_unavailable", cause: "edit_apply_failed", tool, path: filePath });
101645
+ return emitTerminal(failClosedOrAdvisory({
101492
101646
  advisory,
101493
101647
  strict,
101494
101648
  site: "edit_apply_failed",
101495
101649
  why: derived.reason,
101496
101650
  softMsg: `CodeRifts claude-hook: ${derived.reason} \u2014 allowing (soft; never guess edit apply)`,
101497
101651
  errLog
101498
- });
101652
+ }));
101499
101653
  }
101500
101654
  if (diskBefore === derived.after) {
101501
- return { exitCode: 0, reason: "identical" };
101655
+ logEv({ type: "detection_skip", signals: ["identical"], tool, path: filePath });
101656
+ return emitTerminal({ exitCode: 0, reason: "identical" });
101502
101657
  }
101658
+ logEv({ type: "preflight_start", tool, path: filePath });
101503
101659
  let result;
101504
101660
  try {
101505
101661
  result = await authorize(diskBefore, derived.after, apiKey, {
@@ -101508,54 +101664,75 @@ var require_claude_hook = __commonJS({
101508
101664
  });
101509
101665
  } catch (e) {
101510
101666
  const msg = e && e.message ? String(e.message) : "request failed";
101511
- return failClosedOrAdvisory({
101667
+ logEv({ type: "preflight_unavailable", cause: "api_unreachable", tool, path: filePath });
101668
+ return emitTerminal(failClosedOrAdvisory({
101512
101669
  advisory,
101513
101670
  strict,
101514
101671
  site: "api_unreachable",
101515
101672
  why: `API unreachable: ${msg}`,
101516
101673
  softMsg: `CodeRifts claude-hook: API unreachable (${msg}) \u2014 allowing (soft; availability must not brick the editor)`,
101517
101674
  errLog
101518
- });
101675
+ }));
101519
101676
  }
101520
101677
  const mapped = mapDecisionSeverity(result);
101678
+ const fingerprint = extractFingerprint(result);
101521
101679
  const idPart = mapped.decisionId ? ` decision_id=${mapped.decisionId}` : "";
101522
101680
  const decPart = mapped.decision ? ` decision=${mapped.decision}` : "";
101523
101681
  const eaPart = mapped.executionAction ? ` execution_action=${mapped.executionAction}` : "";
101682
+ const actionForLog = mapped.executionAction || (mapped.severity === "BLOCK" || mapped.severity === "UNKNOWN" || mapped.severity === "INDETERMINATE" ? "STOP" : mapped.severity === "REQUIRE_APPROVAL" ? "REQUEST_APPROVAL" : mapped.severity === "MONITOR" || mapped.severity === "WARN" ? "CONTINUE_WITH_MONITORING" : "CONTINUE");
101683
+ logEv({
101684
+ type: "preflight_result",
101685
+ action: actionForLog,
101686
+ decisionId: mapped.decisionId || void 0,
101687
+ fingerprint,
101688
+ tool,
101689
+ path: filePath
101690
+ });
101524
101691
  if (mapped.severity === "INDETERMINATE") {
101525
101692
  errLog(
101526
101693
  "CodeRifts claude-hook: BLOCKED (indeterminate response \u2014 no execution_action and no decision; not permission)"
101527
101694
  );
101528
- return {
101695
+ return emitTerminal({
101529
101696
  exitCode: 2,
101530
101697
  reason: "indeterminate",
101531
101698
  severity: "INDETERMINATE",
101532
101699
  decision: mapped.decision,
101533
101700
  decisionId: mapped.decisionId
101534
- };
101701
+ });
101535
101702
  }
101536
101703
  if (mapped.severity === "BLOCK" || mapped.severity === "UNKNOWN") {
101704
+ const why = renderDecisionWhy(result);
101537
101705
  errLog(
101538
- `CodeRifts claude-hook: BLOCKED${decPart}${eaPart}${idPart}` + (mapped.severity === "UNKNOWN" ? " (unrecognised execution_action)" : "")
101706
+ [
101707
+ `CodeRifts claude-hook: BLOCKED${decPart}${eaPart}${idPart}` + (mapped.severity === "UNKNOWN" ? " (unrecognised execution_action)" : ""),
101708
+ ...why.lines
101709
+ ].join("\n")
101539
101710
  );
101540
- return {
101711
+ return emitTerminal({
101541
101712
  exitCode: 2,
101542
101713
  reason: mapped.severity === "UNKNOWN" ? "unknown_action" : "block",
101543
101714
  severity: mapped.severity,
101544
101715
  decision: mapped.decision,
101545
- decisionId: mapped.decisionId
101546
- };
101716
+ decisionId: mapped.decisionId,
101717
+ reasons: why.reasonCodes
101718
+ });
101547
101719
  }
101548
101720
  if (mapped.severity === "REQUIRE_APPROVAL") {
101721
+ const why = renderDecisionWhy(result);
101549
101722
  errLog(
101550
- `CodeRifts claude-hook: BLOCKED approval_required${decPart}${eaPart}${idPart}`
101723
+ [
101724
+ `CodeRifts claude-hook: BLOCKED approval_required${decPart}${eaPart}${idPart}`,
101725
+ ...why.lines
101726
+ ].join("\n")
101551
101727
  );
101552
- return {
101728
+ return emitTerminal({
101553
101729
  exitCode: 2,
101554
101730
  reason: "approval_required",
101555
101731
  severity: mapped.severity,
101556
101732
  decision: mapped.decision,
101557
- decisionId: mapped.decisionId
101558
- };
101733
+ decisionId: mapped.decisionId,
101734
+ reasons: why.reasonCodes
101735
+ });
101559
101736
  }
101560
101737
  if (mapped.severity === "MONITOR" || mapped.severity === "WARN") {
101561
101738
  const sinkWired = isMonitoringSinkWired({ env, deps, result });
@@ -101563,29 +101740,29 @@ var require_claude_hook = __commonJS({
101563
101740
  errLog(
101564
101741
  `CodeRifts claude-hook: BLOCKED monitoring_unwired${decPart}${eaPart}${idPart} \u2014 CONTINUE_WITH_MONITORING requires CODERIFTS_MONITORING_SINK_WIRED=1 (host assertion; not delivery proof)`
101565
101742
  );
101566
- return {
101743
+ return emitTerminal({
101567
101744
  exitCode: 2,
101568
101745
  reason: "monitoring_unwired",
101569
101746
  severity: mapped.severity === "WARN" ? "MONITOR" : mapped.severity,
101570
101747
  decision: mapped.decision,
101571
101748
  decisionId: mapped.decisionId
101572
- };
101749
+ });
101573
101750
  }
101574
- return {
101751
+ return emitTerminal({
101575
101752
  exitCode: 0,
101576
101753
  reason: "allow",
101577
101754
  severity: mapped.severity === "WARN" ? "MONITOR" : mapped.severity,
101578
101755
  decision: mapped.decision,
101579
101756
  decisionId: mapped.decisionId
101580
- };
101757
+ });
101581
101758
  }
101582
- return {
101759
+ return emitTerminal({
101583
101760
  exitCode: 0,
101584
101761
  reason: "allow",
101585
101762
  severity: "ALLOW",
101586
101763
  decision: mapped.decision,
101587
101764
  decisionId: mapped.decisionId
101588
- };
101765
+ });
101589
101766
  }
101590
101767
  module2.exports = {
101591
101768
  runClaudeHook,
@@ -101593,6 +101770,7 @@ var require_claude_hook = __commonJS({
101593
101770
  isSpecPath,
101594
101771
  deriveAfterContent,
101595
101772
  mapDecisionSeverity,
101773
+ renderDecisionWhy,
101596
101774
  resolveApiKey,
101597
101775
  resolveSpecPath,
101598
101776
  readGitConfig,
@@ -101604,7 +101782,10 @@ var require_claude_hook = __commonJS({
101604
101782
  softOrStrictBlock,
101605
101783
  DEFAULT_SPEC_PATH,
101606
101784
  CLOSED_ACTIONS,
101607
- USAGE
101785
+ USAGE,
101786
+ appendGuardEventLog,
101787
+ extractFingerprint,
101788
+ HOOK_TRIGGER_SOURCE
101608
101789
  };
101609
101790
  }
101610
101791
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coderifts",
3
- "version": "4.0.0",
3
+ "version": "4.1.1",
4
4
  "description": "Detect breaking API changes from the command line. Works locally or with the CodeRifts cloud API.",
5
5
  "author": "CodeRifts <hello@coderifts.com>",
6
6
  "license": "MIT",