coderifts 4.0.0 → 4.1.0

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,20 @@
1
1
  # Changelog
2
2
 
3
+ ## 4.1.0
4
+
5
+ ### Added
6
+
7
+ - **`coderifts claude-hook` JSONL GuardEvent log.** When `CODERIFTS_GUARD_EVENT_LOG` is
8
+ set to an absolute file path, the hook appends one JSON object per line:
9
+ `preflight_start` / `preflight_result` / `preflight_unavailable` / `detection_skip`
10
+ plus terminal `hook_blocked` (exit 2) / `hook_continue` (exit 0) /
11
+ `hook_advisory_passthrough` (ADVISORY exit 0). Extra fields: `tool`, `path`,
12
+ `trigger_source:"claude_hook"`, `fingerprint` when the authorize envelope
13
+ returns one, `sessionId` (`CODERIFTS_SESSION_ID` or per-process). Unset env
14
+ → no file. Log write errors are swallowed and printed to stderr; **exit
15
+ codes are unchanged**. The hook does not claim `enforced` (no
16
+ receipt-verified execution path).
17
+
3
18
  ## 4.0.0
4
19
 
5
20
  **Breaking** — `coderifts claude-hook` is fail-closed by default. The CLI now
@@ -28,6 +43,15 @@ depends on `@coderifts/agent-guard` ^8.1.1.
28
43
  (never widen).
29
44
  - **`MIN_GUARD_MAJOR`:** 6 → 8 (`scripts/assert-guard-major.js`).
30
45
 
46
+ ### Added
47
+ - **`coderifts agent-setup` writes `.claude/settings.json`.** PreToolUse hook
48
+ (`matcher: Write|Edit|MultiEdit` → `coderifts claude-hook`, timeout 60)
49
+ beside the six rule files. Fail-closed default (P0-3/P0-4). Explicit
50
+ opt-out documented on stdout: `CODERIFTS_ADVISORY=1`. `--check` is
51
+ byte-stable against the embed (same drift-gate as the six rule files).
52
+ Existing `.claude/settings.json` is skipped (no merge); `--force`
53
+ replaces the **entire** file.
54
+
31
55
  ### Fixed
32
56
  - **Installer copy (P0 #9 / e76e846):** `agent-setup` / `init` teach
33
57
  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.0",
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
  });
@@ -101248,6 +101274,33 @@ var require_claude_hook = __commonJS({
101248
101274
  if (dr && typeof dr === "object" && dr.monitoringSinkWired === true) return true;
101249
101275
  return false;
101250
101276
  }
101277
+ var HOOK_TRIGGER_SOURCE = "claude_hook";
101278
+ function extractFingerprint(result) {
101279
+ const d = result && typeof result === "object" && !Array.isArray(result) ? result : {};
101280
+ const dr = d.decision_result && typeof d.decision_result === "object" ? d.decision_result : {};
101281
+ const fp = dr.fingerprint || d.fingerprint || d.verdict_fingerprint || d.input_fingerprint;
101282
+ return typeof fp === "string" && fp.length > 0 ? fp : void 0;
101283
+ }
101284
+ function hookSessionId(env) {
101285
+ const fromEnv = env && env.CODERIFTS_SESSION_ID && String(env.CODERIFTS_SESSION_ID).trim();
101286
+ if (fromEnv) return fromEnv;
101287
+ if (!hookSessionId._gen) hookSessionId._gen = `hook-${process.pid}-${Date.now()}`;
101288
+ return hookSessionId._gen;
101289
+ }
101290
+ function appendGuardEventLog(env, event, deps = {}) {
101291
+ const p = env && env.CODERIFTS_GUARD_EVENT_LOG;
101292
+ if (p == null || String(p).trim() === "") return;
101293
+ const errLog = deps.errLog || ((m) => console.error(String(m)));
101294
+ const append = deps.appendFileSync || ((file, data) => fs.appendFileSync(file, data));
101295
+ try {
101296
+ append(String(p).trim(), JSON.stringify(event) + "\n");
101297
+ } catch (err) {
101298
+ try {
101299
+ errLog(`CodeRifts claude-hook: CODERIFTS_GUARD_EVENT_LOG write failed (${err && err.message})`);
101300
+ } catch {
101301
+ }
101302
+ }
101303
+ }
101251
101304
  function failClosedOrAdvisory({ advisory, strict, site, softMsg, why, errLog }) {
101252
101305
  if (advisory && !strict) {
101253
101306
  errLog(`${softMsg} (CODERIFTS_ADVISORY)`);
@@ -101443,32 +101496,65 @@ var require_claude_hook = __commonJS({
101443
101496
  const env = deps.env || process.env;
101444
101497
  const strict = isStrictMode(env);
101445
101498
  const advisory = isAdvisoryMode(env);
101499
+ const sessionId = hookSessionId(env);
101500
+ const logEv = (partial) => {
101501
+ appendGuardEventLog(env, {
101502
+ at: (/* @__PURE__ */ new Date()).toISOString(),
101503
+ sessionId,
101504
+ trigger_source: HOOK_TRIGGER_SOURCE,
101505
+ ...partial
101506
+ }, { errLog, appendFileSync: deps.appendFileSync });
101507
+ };
101508
+ let tool = null;
101509
+ let filePathKnown = null;
101510
+ const emitTerminal = (r) => {
101511
+ const extra = {
101512
+ tool: tool || void 0,
101513
+ path: filePathKnown || void 0,
101514
+ decision: r.decision != null ? r.decision : void 0,
101515
+ decisionId: r.decisionId != null ? r.decisionId : void 0
101516
+ };
101517
+ if (r.exitCode === 2) {
101518
+ logEv({ type: "hook_blocked", exit: 2, ...extra, cause: r.reason || r.site });
101519
+ } else if (r.exitCode === 0 && r.site && advisory && !strict) {
101520
+ logEv({ type: "hook_advisory_passthrough", exit: 0, decision: r.decision || null, ...extra, cause: r.site });
101521
+ } else {
101522
+ logEv({ type: "hook_continue", exit: 0, ...extra, cause: r.reason });
101523
+ }
101524
+ return r;
101525
+ };
101446
101526
  const apiKey = resolveApiKey({ ...deps, cwd, env });
101447
101527
  if (!apiKey) {
101448
- return failClosedOrAdvisory({
101528
+ logEv({ type: "preflight_unavailable", cause: "missing_api_key" });
101529
+ return emitTerminal(failClosedOrAdvisory({
101449
101530
  advisory,
101450
101531
  strict,
101451
101532
  site: "missing_api_key",
101452
101533
  why: "no API key",
101453
101534
  softMsg: "CodeRifts claude-hook: no API key (coderifts login / CODERIFTS_API_KEY / git config coderifts.apiKey) \u2014 allowing",
101454
101535
  errLog
101455
- });
101536
+ }));
101456
101537
  }
101457
101538
  const raw = typeof options.stdin === "string" ? options.stdin : readStdin();
101458
101539
  const parsed = parseStdinJson(raw);
101459
101540
  if (!parsed.ok) {
101460
101541
  errLog(`CodeRifts claude-hook: ${parsed.reason} \u2014 allowing (soft; never block on parse gap)`);
101461
- return { exitCode: 0, reason: "stdin_unparseable" };
101542
+ logEv({ type: "detection_skip", signals: ["stdin_unparseable"] });
101543
+ return emitTerminal({ exitCode: 0, reason: "stdin_unparseable" });
101462
101544
  }
101463
101545
  const { toolName, toolInput } = parsed;
101546
+ tool = toolName;
101464
101547
  const filePath = toolInput.file_path || toolInput.filePath || toolInput.path;
101465
101548
  if (!filePath || typeof filePath !== "string") {
101466
101549
  errLog("CodeRifts claude-hook: tool_input.file_path missing \u2014 allowing (soft)");
101467
- return { exitCode: 0, reason: "missing_file_path" };
101550
+ logEv({ type: "detection_skip", signals: ["missing_file_path"], tool });
101551
+ return emitTerminal({ exitCode: 0, reason: "missing_file_path" });
101468
101552
  }
101553
+ filePathKnown = filePath;
101469
101554
  const specPath = resolveSpecPath({ ...deps, cwd });
101470
101555
  if (!isSpecPath(filePath, specPath)) {
101471
- return { exitCode: 0, reason: "not_spec_path" };
101556
+ logEv({ type: "detection_skip", signals: ["not_spec_path"], tool, path: filePath });
101557
+ return emitTerminal({ exitCode: 0, reason: "not_spec_path" });
101472
101558
  }
101473
101559
  const absPath = path.isAbsolute(filePath) ? filePath : path.resolve(cwd, filePath);
101474
101560
  let diskBefore = "";
@@ -101476,30 +101562,34 @@ var require_claude_hook = __commonJS({
101476
101562
  try {
101477
101563
  diskBefore = readFile(absPath, "utf8");
101478
101564
  } catch (e) {
101479
- return failClosedOrAdvisory({
101565
+ logEv({ type: "preflight_unavailable", cause: "disk_unreadable", tool, path: filePath });
101566
+ return emitTerminal(failClosedOrAdvisory({
101480
101567
  advisory,
101481
101568
  strict,
101482
101569
  site: "disk_unreadable",
101483
101570
  why: `cannot read contract file ${absPath}`,
101484
101571
  softMsg: `CodeRifts claude-hook: cannot read ${absPath} \u2014 allowing (soft)`,
101485
101572
  errLog
101486
- });
101573
+ }));
101487
101574
  }
101488
101575
  }
101489
101576
  const derived = deriveAfterContent(toolName, toolInput, diskBefore);
101490
101577
  if (!derived.ok) {
101491
- return failClosedOrAdvisory({
101578
+ logEv({ type: "preflight_unavailable", cause: "edit_apply_failed", tool, path: filePath });
101579
+ return emitTerminal(failClosedOrAdvisory({
101492
101580
  advisory,
101493
101581
  strict,
101494
101582
  site: "edit_apply_failed",
101495
101583
  why: derived.reason,
101496
101584
  softMsg: `CodeRifts claude-hook: ${derived.reason} \u2014 allowing (soft; never guess edit apply)`,
101497
101585
  errLog
101498
- });
101586
+ }));
101499
101587
  }
101500
101588
  if (diskBefore === derived.after) {
101501
- return { exitCode: 0, reason: "identical" };
101589
+ logEv({ type: "detection_skip", signals: ["identical"], tool, path: filePath });
101590
+ return emitTerminal({ exitCode: 0, reason: "identical" });
101502
101591
  }
101592
+ logEv({ type: "preflight_start", tool, path: filePath });
101503
101593
  let result;
101504
101594
  try {
101505
101595
  result = await authorize(diskBefore, derived.after, apiKey, {
@@ -101508,54 +101598,65 @@ var require_claude_hook = __commonJS({
101508
101598
  });
101509
101599
  } catch (e) {
101510
101600
  const msg = e && e.message ? String(e.message) : "request failed";
101511
- return failClosedOrAdvisory({
101601
+ logEv({ type: "preflight_unavailable", cause: "api_unreachable", tool, path: filePath });
101602
+ return emitTerminal(failClosedOrAdvisory({
101512
101603
  advisory,
101513
101604
  strict,
101514
101605
  site: "api_unreachable",
101515
101606
  why: `API unreachable: ${msg}`,
101516
101607
  softMsg: `CodeRifts claude-hook: API unreachable (${msg}) \u2014 allowing (soft; availability must not brick the editor)`,
101517
101608
  errLog
101518
- });
101609
+ }));
101519
101610
  }
101520
101611
  const mapped = mapDecisionSeverity(result);
101612
+ const fingerprint = extractFingerprint(result);
101521
101613
  const idPart = mapped.decisionId ? ` decision_id=${mapped.decisionId}` : "";
101522
101614
  const decPart = mapped.decision ? ` decision=${mapped.decision}` : "";
101523
101615
  const eaPart = mapped.executionAction ? ` execution_action=${mapped.executionAction}` : "";
101616
+ 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");
101617
+ logEv({
101618
+ type: "preflight_result",
101619
+ action: actionForLog,
101620
+ decisionId: mapped.decisionId || void 0,
101621
+ fingerprint,
101622
+ tool,
101623
+ path: filePath
101624
+ });
101524
101625
  if (mapped.severity === "INDETERMINATE") {
101525
101626
  errLog(
101526
101627
  "CodeRifts claude-hook: BLOCKED (indeterminate response \u2014 no execution_action and no decision; not permission)"
101527
101628
  );
101528
- return {
101629
+ return emitTerminal({
101529
101630
  exitCode: 2,
101530
101631
  reason: "indeterminate",
101531
101632
  severity: "INDETERMINATE",
101532
101633
  decision: mapped.decision,
101533
101634
  decisionId: mapped.decisionId
101534
- };
101635
+ });
101535
101636
  }
101536
101637
  if (mapped.severity === "BLOCK" || mapped.severity === "UNKNOWN") {
101537
101638
  errLog(
101538
101639
  `CodeRifts claude-hook: BLOCKED${decPart}${eaPart}${idPart}` + (mapped.severity === "UNKNOWN" ? " (unrecognised execution_action)" : "")
101539
101640
  );
101540
- return {
101641
+ return emitTerminal({
101541
101642
  exitCode: 2,
101542
101643
  reason: mapped.severity === "UNKNOWN" ? "unknown_action" : "block",
101543
101644
  severity: mapped.severity,
101544
101645
  decision: mapped.decision,
101545
101646
  decisionId: mapped.decisionId
101546
- };
101647
+ });
101547
101648
  }
101548
101649
  if (mapped.severity === "REQUIRE_APPROVAL") {
101549
101650
  errLog(
101550
101651
  `CodeRifts claude-hook: BLOCKED approval_required${decPart}${eaPart}${idPart}`
101551
101652
  );
101552
- return {
101653
+ return emitTerminal({
101553
101654
  exitCode: 2,
101554
101655
  reason: "approval_required",
101555
101656
  severity: mapped.severity,
101556
101657
  decision: mapped.decision,
101557
101658
  decisionId: mapped.decisionId
101558
- };
101659
+ });
101559
101660
  }
101560
101661
  if (mapped.severity === "MONITOR" || mapped.severity === "WARN") {
101561
101662
  const sinkWired = isMonitoringSinkWired({ env, deps, result });
@@ -101563,29 +101664,29 @@ var require_claude_hook = __commonJS({
101563
101664
  errLog(
101564
101665
  `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
101666
  );
101566
- return {
101667
+ return emitTerminal({
101567
101668
  exitCode: 2,
101568
101669
  reason: "monitoring_unwired",
101569
101670
  severity: mapped.severity === "WARN" ? "MONITOR" : mapped.severity,
101570
101671
  decision: mapped.decision,
101571
101672
  decisionId: mapped.decisionId
101572
- };
101673
+ });
101573
101674
  }
101574
- return {
101675
+ return emitTerminal({
101575
101676
  exitCode: 0,
101576
101677
  reason: "allow",
101577
101678
  severity: mapped.severity === "WARN" ? "MONITOR" : mapped.severity,
101578
101679
  decision: mapped.decision,
101579
101680
  decisionId: mapped.decisionId
101580
- };
101681
+ });
101581
101682
  }
101582
- return {
101683
+ return emitTerminal({
101583
101684
  exitCode: 0,
101584
101685
  reason: "allow",
101585
101686
  severity: "ALLOW",
101586
101687
  decision: mapped.decision,
101587
101688
  decisionId: mapped.decisionId
101588
- };
101689
+ });
101589
101690
  }
101590
101691
  module2.exports = {
101591
101692
  runClaudeHook,
@@ -101604,7 +101705,10 @@ var require_claude_hook = __commonJS({
101604
101705
  softOrStrictBlock,
101605
101706
  DEFAULT_SPEC_PATH,
101606
101707
  CLOSED_ACTIONS,
101607
- USAGE
101708
+ USAGE,
101709
+ appendGuardEventLog,
101710
+ extractFingerprint,
101711
+ HOOK_TRIGGER_SOURCE
101608
101712
  };
101609
101713
  }
101610
101714
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "coderifts",
3
- "version": "4.0.0",
3
+ "version": "4.1.0",
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",