@coderifts/agent-hooks 0.3.0 → 0.3.2

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agent-hooks",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Ask CodeRifts before Claude Code writes a contract artifact (OpenAPI, AsyncAPI, GraphQL, protobuf, MCP manifest).",
5
5
  "author": {
6
6
  "name": "CodeRifts",
package/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.3.2 — 2026-10-06
4
+ ### Fixed
5
+ - **An MCP client configuration is never treated as a contract (contract-write 1.2.0, P65).** `.mcp.json`, `mcp.json`
6
+ (also under `.cursor/` and `.vscode/`), `claude_desktop_config.json` and `(cline_)mcp_settings.json`: an edit of one
7
+ sent its whole text, the servers' `env` credentials included, to CodeRifts preflight. They are decided by name and
8
+ never read; MCP tool manifests are unchanged.
9
+
10
+ ## 0.3.1 — 2026-10-04
11
+
12
+ ### Changed
13
+
14
+ - **When CodeRifts could not decide, the refusal says so in one sentence** — the one the GitHub App, `coderifts claude-hook` and the CodeRifts mod write: `GOVERNANCE_UNAVAILABLE: CodeRifts could not decide (<why>); this is not a finding about your change.` It is added to the unreachable and unreadable-answer approvals (`gate.js`) and to the Claude hook's fail-closed messages (`claude-code/hook.mjs`); nothing that stopped now passes. `governanceUnavailable(why)` is exported from `gate.js`.
15
+
3
16
  ## 0.3.0 — 2026-10-03
4
17
 
5
18
  ### Changed
@@ -19,7 +19,7 @@
19
19
  import { resolve } from "node:path";
20
20
  import { pathToFileURL } from "node:url";
21
21
  import { afterText, decideToolCall } from "../contract-write.mjs";
22
- import { createGate, DOES_NOT_PROVE, hostIo } from "../gate.js";
22
+ import { createGate, DOES_NOT_PROVE, governanceUnavailable, hostIo } from "../gate.js";
23
23
 
24
24
  /** The file after an Edit (`params` is the edit) or a MultiEdit (`params.edits`, in order): contract-write's afterText. */
25
25
  export function applyEdits(params, before) {
@@ -135,7 +135,7 @@ export async function runClaudeHook(stdinText, { env = process.env, deps } = {})
135
135
  const r = await shellDecision(String(payload.tool_input?.command ?? ""), hostIo(payload.cwd, deps?.readFile, deps?.listDir));
136
136
  return r ? jsonDecision(r.decision, r.text) : emptyPass();
137
137
  } catch (err) {
138
- return failClosed(`CodeRifts Claude hook failed on a Bash call (${String(err?.message ?? err)}). Not knowing is not permission.`);
138
+ return failClosed(`CodeRifts Claude hook failed on a Bash call (${String(err?.message ?? err)}). ${governanceUnavailable("the Bash check failed")} Not knowing is not permission.`);
139
139
  }
140
140
  }
141
141
 
@@ -151,7 +151,7 @@ export async function runClaudeHook(stdinText, { env = process.env, deps } = {})
151
151
  return mapGateResult(result);
152
152
  } catch (err) {
153
153
  return failClosed(
154
- `CodeRifts Claude hook failed before a decision (${String(err?.message ?? err)}). Not knowing is not permission.`,
154
+ `CodeRifts Claude hook failed before a decision (${String(err?.message ?? err)}). ${governanceUnavailable("the hook failed before a decision")} Not knowing is not permission.`,
155
155
  );
156
156
  }
157
157
  }
@@ -5,7 +5,7 @@
5
5
  "repo": "coderifts/agent-hooks"
6
6
  },
7
7
  "description": "Host adapters for CodeRifts: Claude Code PreToolUse asks before Write/Edit of OpenAPI, AsyncAPI, GraphQL, protobuf, or MCP manifest files. Not the GitHub Action (coderifts/contract-gate) and not the guard library (@coderifts/agent-guard).",
8
- "version": "0.3.0",
8
+ "version": "0.3.2",
9
9
  "author": {
10
10
  "name": "CodeRifts",
11
11
  "email": "peter@coderifts.com"
@@ -27,7 +27,7 @@
27
27
  // written by scripts/generate-contract-write-copies.js, byte for byte (the CommonJS twin is a
28
28
  // mechanical transform), and its --check fails on any difference. Do not edit a copy.
29
29
 
30
- export const CONTRACT_WRITE_VERSION = '1.1.0';
30
+ export const CONTRACT_WRITE_VERSION = '1.2.0';
31
31
 
32
32
  /** What a shell write to a named contract file gets, and what an unnamed one gets. */
33
33
  export const SHELL_NAMED_DECISION = 'refuse';
@@ -41,9 +41,21 @@ export const LISTING_LIMITS = Object.freeze({ depth: 8, entries: 5000 });
41
41
  /** The @coderifts/contract-path list (index.cjs), word for word; test/contract-write.test.js holds them equal. */
42
42
  export const CONTRACT_EXT = /\.(ya?ml|json|graphql|gql|proto)$/i;
43
43
 
44
+ /*
45
+ * 1.2.0 (2026-10-06, the Claude directory's hold MCP_FORWARDS_CREDENTIAL_ENV): an MCP CLIENT
46
+ * configuration file — the list of servers a client starts, with their `env` credentials — is not a
47
+ * contract. `.mcp.json` and `mcp.json` matched the list above (".json" + "mcp") as mcp_manifest, so an
48
+ * Edit of one sent its whole text, tokens included, to preflight. Decided by name, so the file is never
49
+ * read to decide; it wins over the project's own `schema:` list too. MCP tool manifests are unchanged.
50
+ * P65 (2026-10-06): the required check uses this same pattern — index.cjs requires it from here, and its
51
+ * looksLikeContractPath says no first, word for word as below.
52
+ */
53
+ export const MCP_CLIENT_CONFIG = /(^|\/)(\.?mcp\.json|claude_desktop_config\.json|(cline_)?mcp_settings\.json)$/i;
54
+
44
55
  export function looksLikeContractPath(p) {
45
56
  const s = String(p || '').toLowerCase();
46
57
  if (s.includes('node_modules/') || s.includes('vendor/')) return false;
58
+ if (MCP_CLIENT_CONFIG.test(s)) return false;
47
59
  return CONTRACT_EXT.test(s) && (s.includes('openapi') || s.includes('swagger') || s.includes('asyncapi')
48
60
  || s.endsWith('.graphql') || s.endsWith('.gql') || s.endsWith('.proto') || s.includes('mcp'));
49
61
  }
@@ -68,6 +80,7 @@ const HOOK_EXTRAS = Object.freeze([
68
80
  [/(^|\/)tools\.(wire\.v1\.)?json$/i, 'mcp_manifest'],
69
81
  ]);
70
82
 
83
+
71
84
  /** `a/./b/../c` → `a/c`; backslashes become slashes; a leading `./` goes. */
72
85
  export function normalizePath(p) {
73
86
  const s = String(p || '').replace(/\\/g, '/');
@@ -114,6 +127,7 @@ export function contractType(p, { named = [], excluded = [] } = {}) {
114
127
  const rel = normalizePath(p);
115
128
  if (!rel) return null;
116
129
  if (excluded.some((x) => matchesPattern(rel, x))) return null;
130
+ if (MCP_CLIENT_CONFIG.test(rel)) return null;
117
131
  for (const [re, type] of HOOK_EXTRAS) if (re.test(rel) && !/(^|\/)(node_modules|vendor)\//i.test(rel)) return type;
118
132
  if (looksLikeContractPath(rel)) return typeForPath(rel);
119
133
  if (named.some((x) => matchesPattern(rel, x))) return typeForPath(rel);
package/gate.js CHANGED
@@ -122,6 +122,15 @@ export async function askCodeRifts({ endpoint, apiKey, timeoutMs, operation, art
122
122
  }
123
123
  }
124
124
 
125
+ /**
126
+ * GOVERNANCE_UNAVAILABLE (2026-10-04): the one sentence the App, the CLI hook and the Claude Code mod
127
+ * write verbatim when CodeRifts could not decide. The call still stops; the sentence says the stop is
128
+ * about CodeRifts, not about the change.
129
+ */
130
+ export function governanceUnavailable(why) {
131
+ return `GOVERNANCE_UNAVAILABLE: CodeRifts could not decide (${why}); this is not a finding about your change.`;
132
+ }
133
+
125
134
  /** Ask for a human, always with a reason. Never `{}`. */
126
135
  const ask = (title, description) => ({
127
136
  requireApproval: {
@@ -146,6 +155,7 @@ export function decide(outcome, { operation, path }) {
146
155
  return ask(
147
156
  "CodeRifts did not answer",
148
157
  `${path} is a contract artifact and CodeRifts could not be reached (${outcome.detail}). ` +
158
+ `${governanceUnavailable(`CodeRifts could not be reached: ${outcome.detail}`)} ` +
149
159
  `No decision was obtained, and not knowing is not permission.`,
150
160
  );
151
161
  }
@@ -153,7 +163,8 @@ export function decide(outcome, { operation, path }) {
153
163
  return ask(
154
164
  "CodeRifts answer could not be read",
155
165
  `${path} is a contract artifact and CodeRifts replied with something this gate cannot interpret ` +
156
- `(${outcome.detail}). No decision was obtained, and not knowing is not permission.`,
166
+ `(${outcome.detail}). ${governanceUnavailable(`the answer could not be read: ${outcome.detail}`)} ` +
167
+ `No decision was obtained, and not knowing is not permission.`,
157
168
  );
158
169
  }
159
170
 
@@ -2,7 +2,7 @@
2
2
  "id": "coderifts-contract-gate",
3
3
  "name": "CodeRifts contract gate",
4
4
  "description": "Ask CodeRifts before an agent writes a contract artifact.",
5
- "version": "0.3.0",
5
+ "version": "0.3.2",
6
6
  "configSchema": {
7
7
  "type": "object",
8
8
  "additionalProperties": false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@coderifts/agent-hooks",
3
- "version": "0.3.0",
3
+ "version": "0.3.2",
4
4
  "description": "Ask CodeRifts before an agent writes a contract artifact (OpenClaw before_tool_call and Claude Code PreToolUse).",
5
5
  "license": "MIT",
6
6
  "homepage": "https://github.com/coderifts/agent-hooks#readme",