arkgate 4.5.6 → 4.5.7

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
@@ -5,6 +5,29 @@ in the immutable pre-2.0 archive linked below.
5
5
 
6
6
  ## Unreleased
7
7
 
8
+ ## 4.5.7 — 2026-08-12
9
+
10
+ **Patch** over **4.5.6**. Cursor hard write path: project `.cursor/hooks.json` `preToolUse`
11
+ gate for `Write` / `StrReplace` (failClosed), host matrix + doctor honesty, payload mapping
12
+ (`contents` / `StrReplace`), install/merge without wiping sibling hooks. Repair envelope may
13
+ emit; Write `updated_input` reinjection is **not** claimed. Codex/OpenCode stay advisory at
14
+ write. Shell/Tab/human edits still rely on required CI. **No required config migration.**
15
+
16
+ **Status: prepared** (not published).
17
+
18
+ ### Added
19
+
20
+ - **Cursor hard write (CH01):** install writes `.cursor/hooks.json` with
21
+ `preToolUse` matcher `Write|StrReplace`, `failClosed: true`, `--hook --hook-repair`,
22
+ `--root-env CURSOR_PROJECT_DIR`. Merge preserves sibling Cursor hooks.
23
+ - **Hook runtime:** Cursor payload normalization (`contents` → `content`, `StrReplace` → Edit)
24
+ and stdout `{ permission: "deny", agent_message, user_message }` on block.
25
+
26
+ ### Changed
27
+
28
+ - Host support matrix: Cursor `hard-write: true` for listed ops; soft hosts are Codex/OpenCode.
29
+ - Docs: `docs/ai-gates.md`, agent guide, README matrix, CLI help.
30
+
8
31
  ## 4.5.6 — 2026-08-11
9
32
 
10
33
  **Patch** over **4.5.5**. Field upgrade & multi-project MCP truth (Phase FX): registry-aware
@@ -12,7 +35,7 @@ package install, structured skip recovery, skill drift + opt-in refresh, post-up
12
35
  MCP process package honesty, host selection notes, early whatsNew, inventory UX-copy quieting.
13
36
  **No required config migration.** No new skill names, sensors, or scores. Freezes held.
14
37
 
15
- **Status: prepared** (not on npm `latest` until publish train completes; see
38
+ **Status: published** (on npm `latest`; see
16
39
  `docs/releases/4.5.6.md`).
17
40
 
18
41
  ### Added
package/README.md CHANGED
@@ -16,8 +16,8 @@ and makes sure a “green” check means something real.
16
16
 
17
17
  </div>
18
18
 
19
- > **ArkGate 4.5.5** is on npm `latest` — deep-module coach, upgrade what’s new, session honesty.
20
- > [4.5.5 notes](docs/releases/4.5.5.md) · [4.5.0](docs/releases/4.5.0.md) · [4.4.0](docs/releases/4.4.0.md) · [Docs hub](docs/README.md) · [Product voice](docs/product-voice.md)
19
+ > **ArkGate 4.5.6** is on npm `latest` — field upgrade truth, multi-project MCP honesty, skill drift.
20
+ > [4.5.6 notes](docs/releases/4.5.6.md) · [4.5.5](docs/releases/4.5.5.md) · [4.5.0](docs/releases/4.5.0.md) · [Docs hub](docs/README.md) · [Product voice](docs/product-voice.md)
21
21
 
22
22
  ---
23
23
 
@@ -113,16 +113,16 @@ Details: [docs/use.md](docs/use.md).
113
113
  | Claude Code | **Hard** block for listed ops (PreToolUse `Write` / `Edit` / `MultiEdit`) when installed + trusted | Advisory; the agent must call it | **Required GitHub status context** running `arkgate-check --strict-merge` (alias `ark-check`) | Emitted on hook deny; host must re-inject (hard path when installed + trusted) |
114
114
  | Grok Build | **Hard** block for listed ops (PreToolUse `write` / `search_replace` (plus aliases)) when installed + trusted | Advisory; the agent must call it | **Required GitHub status context** running `arkgate-check --strict-merge` (alias `ark-check`) | Emitted on hook deny; host must re-inject (hard path when installed + trusted) |
115
115
  | Google Antigravity | **Hard** block for listed ops (PreToolUse `write_to_file` / `replace_file_content` / `multi_replace_file_content`) when installed + trusted | Advisory; the agent must call it | **Required GitHub status context** running `arkgate-check --strict-merge` (alias `ark-check`) | Emitted on hook deny; host must re-inject (hard path when installed + trusted) |
116
- | Cursor | **Advisory only** at write (no hard hook) | Advisory; the agent must call it | **Required GitHub status context** running `arkgate-check --strict-merge` (alias `ark-check`) | No hard-boundary payload |
117
- | OpenAI Codex | **Advisory / best-effort** at write (not equivalent to Claude/Grok hard block) | Advisory; the agent must call it | **Required GitHub status context** running `arkgate-check --strict-merge` (alias `ark-check`) | Envelope may emit (`--hook-repair`); reinjection **not** guaranteed (advisory host) |
116
+ | Cursor | **Hard** block for listed ops (preToolUse `Write` / `StrReplace`) when installed + trusted | Advisory; the agent must call it | **Required GitHub status context** running `arkgate-check --strict-merge` (alias `ark-check`) | Envelope may emit (`--hook-repair`); reinjection **not** guaranteed (advisory host) |
117
+ | OpenAI Codex | **Advisory / best-effort** at write (not equivalent to Claude/Grok/Cursor hard block) | Advisory; the agent must call it | **Required GitHub status context** running `arkgate-check --strict-merge` (alias `ark-check`) | Envelope may emit (`--hook-repair`); reinjection **not** guaranteed (advisory host) |
118
118
  | OpenCode | **Advisory / best-effort** at write (MCP + optional plugin; not a hard boundary) | Advisory; the agent must call it | **Required GitHub status context** running `arkgate-check --strict-merge` (alias `ark-check`) | No hard-boundary payload |
119
119
 
120
120
  **Read the CI column:** for every host, the repository-wide hard guarantee is a **required**
121
121
  GitHub **status context** that runs the CLI — not “CI file present,” and not the CLI binary name alone.
122
- Cursor/Codex/OpenCode never get a fake hard write claim.
122
+ Codex/OpenCode never get a fake hard write claim. Cursor hard write covers only listed
123
+ `preToolUse` ops when `.cursor/hooks.json` is installed and trusted — Shell/Tab/human edits still rely on CI.
123
124
 
124
125
  This table describes the supported profile **after its files are installed and the host loads/trusts them**. A hard local boundary covers only the listed hook operations; alternate tools, direct filesystem writes, and human edits still rely on CI. MCP validation is advisory because the agent must call it. The CI check blocks a merge only when the repository makes that status required. Repair **envelopes** may be emitted without reinjection being guaranteed; silent auto-apply never happens. Run `arkgate-check --doctor` (or `ark-check --doctor`) for the evidence actually detected in the current repository.
125
-
126
126
  <!-- arkgate-host-support:end -->
127
127
 
128
128
  #### Why the hard guarantee lives at the merge gate
@@ -208,7 +208,7 @@ for real systems. Details: [docs/production-hardening.md](docs/production-harden
208
208
  | Config · package surface · TS | [configuration](docs/configuration.md) · [package-surface](docs/package-surface.md) · [typescript-support](docs/typescript-support.md) |
209
209
  | Brownfield | [docs/brownfield-adoption.md](docs/brownfield-adoption.md) |
210
210
  | Security | [SECURITY.md](SECURITY.md) |
211
- | Current release (4.5.5 on npm `latest`) | [docs/releases/4.5.5.md](docs/releases/4.5.5.md) · [CHANGELOG](CHANGELOG.md) |
211
+ | Current release (4.5.6 on npm `latest`) | [docs/releases/4.5.6.md](docs/releases/4.5.6.md) · [CHANGELOG](CHANGELOG.md) |
212
212
  | Prior (4.5.0) | [docs/releases/4.5.0.md](docs/releases/4.5.0.md) |
213
213
  | Prior (4.4.0) | [docs/releases/4.4.0.md](docs/releases/4.4.0.md) |
214
214
  | Prior (4.3.0) | [docs/releases/4.3.0.md](docs/releases/4.3.0.md) |
@@ -322,9 +322,10 @@ function usage() {
322
322
  'transition. Weakening or judgment-required findings fail unless --policy-ack names',
323
323
  'every finding and is bound to both policy hashes. Use --policy-base/--policy-base-ref',
324
324
  'for an explicit comparison; ARK_POLICY_BASE_REF is the CI environment equivalent.',
325
- 'Add --require-write-hook claude|grok to validate a hard local write boundary for that',
326
- 'specific host. Cursor and Codex expose advisory MCP tools plus the shared CI check;',
327
- 'merge blocking requires repository policy to make that status required.',
325
+ 'Add --require-write-hook claude|grok|antigravity|cursor to validate a hard local write',
326
+ 'boundary for that specific host. Codex and OpenCode expose advisory MCP (plus best-effort',
327
+ 'hooks where applicable) and the shared CI check; merge blocking requires repository policy',
328
+ 'to make that status required.',
328
329
  '',
329
330
  '--require-gates implies --strict-config and fails when the Ark contract in AGENTS.md,',
330
331
  'the project-rooted Ark server in .mcp.json, or fail-closed CI is missing/invalid.',
@@ -332,7 +333,8 @@ function usage() {
332
333
  '',
333
334
  '--install-agent-gates writes AGENTS.md, .mcp.json, and the CI workflow for every',
334
335
  'project, plus tool-specific templates. Known tools: claude, cursor, codex, grok',
335
- '(Claude/Grok hard-write hooks; Cursor/Codex advisory MCP; shared CI check for all) and',
336
+ '(Claude/Grok/Antigravity/Cursor hard-write hooks when covered; Codex advisory MCP;',
337
+ 'shared CI check for all) and',
336
338
  'windsurf, cline, copilot, kiro, roo, continue, gemini',
337
339
  '(instruction-tier rule files derived from the same contract).',
338
340
  'It also installs the /ark-* skills shipped in templates/skills/ into each',
@@ -5,8 +5,11 @@ import path from 'node:path';
5
5
  import readline from 'node:readline';
6
6
  import { createHash, randomUUID } from 'node:crypto';
7
7
  import { spawnSync } from 'node:child_process';
8
- import { createRequire } from 'node:module';
9
8
  import { fileURLToPath } from 'node:url';
9
+ import {
10
+ buildProcessPackageHonesty,
11
+ readProjectInstalledArkgateVersion,
12
+ } from './lib/mcp-process-package.mjs';
10
13
  import {
11
14
  DEFAULT_INTENT_PREFIXES,
12
15
  DEFAULT_LAYER_DIRECTORIES,
@@ -351,6 +354,8 @@ function mapAntigravityToolCall(toolCall) {
351
354
  * Grok Build: { toolName, toolInput: { file_path, content | old_string/new_string } }
352
355
  * (aliases Write/Edit/MultiEdit → write/search_replace; matcher keeps both)
353
356
  * Antigravity: { toolCall: { name, args: { TargetFile, CodeContent, … } } }
357
+ * Cursor: { tool_name, tool_input, hook_event_name?, workspace_roots? }
358
+ * Write uses `contents`; StrReplace maps to Edit (path/old_string/new_string).
354
359
  */
355
360
  function normalizeHookPayload(payload, grokHookEvent = Boolean(process.env.GROK_HOOK_EVENT)) {
356
361
  const antigravityStyle =
@@ -367,17 +372,25 @@ function normalizeHookPayload(payload, grokHookEvent = Boolean(process.env.GROK_
367
372
  toolInput: { ...(mapped?.toolInput ?? {}), file_path: filePath },
368
373
  grokStyle: true, // decision JSON on stdout (deny)
369
374
  antigravityStyle: true,
375
+ cursorStyle: false,
370
376
  operation: mapped?.operation ?? mapped?.toolName ?? null,
371
377
  };
372
378
  }
373
379
 
374
380
  const rawName = payload?.tool_name ?? payload?.toolName ?? '';
375
- const toolInput = payload?.tool_input ?? payload?.toolInput ?? {};
381
+ const toolInputRaw = payload?.tool_input ?? payload?.toolInput ?? {};
382
+ const toolInput =
383
+ toolInputRaw && typeof toolInputRaw === 'object' ? { ...toolInputRaw } : {};
384
+ // Cursor Write uses `contents`; Claude/Grok use `content`.
385
+ if (toolInput.content == null && typeof toolInput.contents === 'string') {
386
+ toolInput.content = toolInput.contents;
387
+ }
376
388
  const nameMap = {
377
389
  Write: 'Write',
378
390
  write: 'Write',
379
391
  Edit: 'Edit',
380
392
  search_replace: 'Edit',
393
+ StrReplace: 'Edit',
381
394
  MultiEdit: 'MultiEdit',
382
395
  ApplyPatch: 'ApplyPatch',
383
396
  apply_patch: 'ApplyPatch',
@@ -388,6 +401,15 @@ function normalizeHookPayload(payload, grokHookEvent = Boolean(process.env.GROK_
388
401
  const toolName = nameMap[rawName] ?? rawName;
389
402
  const filePath =
390
403
  toolInput.file_path ?? toolInput.filePath ?? toolInput.path ?? toolInput.target_file;
404
+ const cursorStyle =
405
+ Boolean(process.env.CURSOR_PROJECT_DIR) ||
406
+ Boolean(process.env.CURSOR_VERSION) ||
407
+ (payload != null &&
408
+ typeof payload === 'object' &&
409
+ (payload.hook_event_name === 'preToolUse' ||
410
+ Array.isArray(payload.workspace_roots) ||
411
+ rawName === 'StrReplace' ||
412
+ (rawName === 'Write' && typeof toolInputRaw?.contents === 'string')));
391
413
  return {
392
414
  toolName,
393
415
  toolInput: { ...toolInput, file_path: filePath },
@@ -396,7 +418,8 @@ function normalizeHookPayload(payload, grokHookEvent = Boolean(process.env.GROK_
396
418
  grokHookEvent ||
397
419
  (payload != null && typeof payload === 'object' && 'toolName' in payload),
398
420
  antigravityStyle: false,
399
- operation: null,
421
+ cursorStyle,
422
+ operation: rawName === 'StrReplace' ? 'StrReplace' : null,
400
423
  };
401
424
  }
402
425
 
@@ -551,7 +574,7 @@ function designDeltaViolations(delta) {
551
574
  * generated code are inserted literally, never interpreted as replacement patterns.
552
575
  */
553
576
  function proposedSource(toolName, toolInput) {
554
- if (toolName === 'Write') return toolInput.content;
577
+ if (toolName === 'Write') return toolInput.content ?? toolInput.contents;
555
578
 
556
579
  let text = '';
557
580
  try {
@@ -607,18 +630,30 @@ function emitAntigravityAllow(output, antigravityStyle) {
607
630
  output.stdout(`${JSON.stringify({ decision: 'allow' })}\n`);
608
631
  }
609
632
 
633
+ /** Cursor preToolUse accepts explicit allow; exit 0 alone also works. */
634
+ function emitCursorAllow(output, cursorStyle) {
635
+ if (!cursorStyle) return;
636
+ output.stdout(`${JSON.stringify({ permission: 'allow' })}\n`);
637
+ }
638
+
639
+ function emitHostAllow(output, { antigravityStyle, cursorStyle }) {
640
+ emitAntigravityAllow(output, antigravityStyle);
641
+ emitCursorAllow(output, cursorStyle);
642
+ }
643
+
610
644
  function runHookPayload(payload, gate, config, args, ts, attemptContext, output = processHookOutput()) {
611
- const { toolName, toolInput, grokStyle, antigravityStyle, operation } = normalizeHookPayload(
612
- payload,
613
- attemptContext?.grokHookEvent ?? Boolean(process.env.GROK_HOOK_EVENT)
614
- );
645
+ const { toolName, toolInput, grokStyle, antigravityStyle, cursorStyle, operation } =
646
+ normalizeHookPayload(
647
+ payload,
648
+ attemptContext?.grokHookEvent ?? Boolean(process.env.GROK_HOOK_EVENT)
649
+ );
615
650
  if (toolName === 'ApplyPatch') {
616
651
  const patch = toolInput.patch ?? toolInput.input ?? toolInput.content;
617
652
  const parsedPatch = codexPatchWrites(patch, args.root);
618
653
  // Codex ApplyPatch is only preflighted when Ark can reconstruct every file operation.
619
654
  // An incomplete reconstruction must not be mislabeled as atomic or hard enforcement.
620
655
  if (!parsedPatch.complete) {
621
- emitAntigravityAllow(output, antigravityStyle);
656
+ emitHostAllow(output, { antigravityStyle, cursorStyle });
622
657
  return;
623
658
  }
624
659
  const patchWrites = parsedPatch.writes;
@@ -684,7 +719,7 @@ function runHookPayload(payload, gate, config, args, ts, attemptContext, output
684
719
  );
685
720
  }
686
721
  if (changes.length === 0) {
687
- emitAntigravityAllow(output, antigravityStyle);
722
+ emitHostAllow(output, { antigravityStyle, cursorStyle });
688
723
  return;
689
724
  }
690
725
  result = prepareChangeFromRoot({
@@ -742,7 +777,7 @@ function runHookPayload(payload, gate, config, args, ts, attemptContext, output
742
777
  ? evaluateWriteDesignDelta({ root: args.root, config, changes, ts })
743
778
  : null;
744
779
  if (result.valid && (designDelta?.valid ?? true)) {
745
- emitAntigravityAllow(output, antigravityStyle);
780
+ emitHostAllow(output, { antigravityStyle, cursorStyle });
746
781
  return;
747
782
  }
748
783
  const message = [
@@ -776,23 +811,23 @@ function runHookPayload(payload, gate, config, args, ts, attemptContext, output
776
811
  const filePath = toolInput.file_path;
777
812
  if (!['Write', 'Edit', 'MultiEdit'].includes(toolName)) {
778
813
  // Non-file tools: fail-open. Antigravity still needs an explicit allow decision.
779
- emitAntigravityAllow(output, antigravityStyle);
814
+ emitHostAllow(output, { antigravityStyle, cursorStyle });
780
815
  return;
781
816
  }
782
817
  if (typeof filePath !== 'string' || !SOURCE_FILE.test(filePath) || filePath.endsWith('.d.ts')) {
783
- emitAntigravityAllow(output, antigravityStyle);
818
+ emitHostAllow(output, { antigravityStyle, cursorStyle });
784
819
  return;
785
820
  }
786
821
  const rel = path.relative(args.root, path.resolve(filePath));
787
822
  const segments = rel.split(path.sep);
788
823
  if (segments[0] === '..' || segments.includes('node_modules')) {
789
- emitAntigravityAllow(output, antigravityStyle);
824
+ emitHostAllow(output, { antigravityStyle, cursorStyle });
790
825
  return;
791
826
  }
792
827
 
793
828
  const source = proposedSource(toolName, toolInput);
794
829
  if (typeof source !== 'string') {
795
- emitAntigravityAllow(output, antigravityStyle);
830
+ emitHostAllow(output, { antigravityStyle, cursorStyle });
796
831
  return;
797
832
  }
798
833
 
@@ -830,7 +865,7 @@ function runHookPayload(payload, gate, config, args, ts, attemptContext, output
830
865
  })
831
866
  : null;
832
867
  if (result.valid && (designDelta?.valid ?? true)) {
833
- emitAntigravityAllow(output, antigravityStyle);
868
+ emitHostAllow(output, { antigravityStyle, cursorStyle });
834
869
  return;
835
870
  }
836
871
 
@@ -861,7 +896,7 @@ function runHookPayload(payload, gate, config, args, ts, attemptContext, output
861
896
  return false;
862
897
  });
863
898
  if (newViolations.length === 0 && (designDelta?.valid ?? true)) {
864
- emitAntigravityAllow(output, antigravityStyle);
899
+ emitHostAllow(output, { antigravityStyle, cursorStyle });
865
900
  return;
866
901
  }
867
902
  const combinedViolations = [...newViolations, ...designDeltaViolations(designDelta)];
@@ -923,7 +958,13 @@ function runHookPayload(payload, gate, config, args, ts, attemptContext, output
923
958
  enforcement: hookEnforcement(
924
959
  args.root,
925
960
  attemptContext?.host ??
926
- (antigravityStyle ? 'antigravity' : grokStyle ? 'grok' : 'claude'),
961
+ (antigravityStyle
962
+ ? 'antigravity'
963
+ : cursorStyle
964
+ ? 'cursor'
965
+ : grokStyle
966
+ ? 'grok'
967
+ : 'claude'),
927
968
  attemptContext?.operation ??
928
969
  operation ??
929
970
  (antigravityStyle
@@ -932,11 +973,15 @@ function runHookPayload(payload, gate, config, args, ts, attemptContext, output
932
973
  : toolName === 'MultiEdit'
933
974
  ? 'multi_replace_file_content'
934
975
  : 'write_to_file'
935
- : grokStyle
976
+ : cursorStyle
936
977
  ? toolName === 'Edit'
937
- ? 'search_replace'
938
- : 'write'
939
- : toolName),
978
+ ? 'StrReplace'
979
+ : 'Write'
980
+ : grokStyle
981
+ ? toolName === 'Edit'
982
+ ? 'search_replace'
983
+ : 'write'
984
+ : toolName),
940
985
  toolName === 'Write' || Boolean(attemptContext?.completePatch)
941
986
  ),
942
987
  ...(layer ? { layer } : {}),
@@ -968,6 +1013,16 @@ function runHookPayload(payload, gate, config, args, ts, attemptContext, output
968
1013
  }) + '\n'
969
1014
  );
970
1015
  }
1016
+ // Cursor preToolUse: permission deny + agent_message (exit 2 also blocks).
1017
+ if (cursorStyle) {
1018
+ output.stdout(
1019
+ JSON.stringify({
1020
+ permission: 'deny',
1021
+ agent_message: message,
1022
+ user_message: `ArkGate blocked write to ${rel}`,
1023
+ }) + '\n'
1024
+ );
1025
+ }
971
1026
  output.status(2);
972
1027
  }
973
1028
 
@@ -1852,47 +1907,13 @@ export async function runArkMcp({ hookInput } = {}) {
1852
1907
  * FX06 — process package vs project install honesty (multi-project field truth).
1853
1908
  * Process arkgateVersion is startup-loaded; after consumer `npm i arkgate@newer`,
1854
1909
  * long-lived MCP can report a stale process version until restart.
1910
+ * Algorithm lives in bin/lib/mcp-process-package.mjs for unit coverage.
1855
1911
  */
1856
- function readProjectInstalledArkgateVersion() {
1857
- try {
1858
- const shallow = path.join(resolvedRoot, 'node_modules', 'arkgate', 'package.json');
1859
- if (fs.existsSync(shallow)) {
1860
- const v = JSON.parse(fs.readFileSync(shallow, 'utf8')).version;
1861
- return typeof v === 'string' && v.trim() ? v.trim() : null;
1862
- }
1863
- } catch {
1864
- /* fall through */
1865
- }
1866
- try {
1867
- const requireFromProject = createRequire(path.join(resolvedRoot, 'package.json'));
1868
- const pkgJson = requireFromProject.resolve('arkgate/package.json');
1869
- const v = JSON.parse(fs.readFileSync(pkgJson, 'utf8')).version;
1870
- return typeof v === 'string' && v.trim() ? v.trim() : null;
1871
- } catch {
1872
- return null;
1873
- }
1874
- }
1875
-
1876
1912
  function processPackageHonesty() {
1877
- const processVersion = typeof ark.version === 'string' ? ark.version : null;
1878
- const projectInstalledVersion = readProjectInstalledArkgateVersion();
1879
- const mismatch =
1880
- processVersion != null &&
1881
- projectInstalledVersion != null &&
1882
- processVersion !== projectInstalledVersion;
1883
- return {
1884
- schemaVersion: '1.0',
1885
- notAScore: true,
1886
- processArkgateVersion: processVersion,
1887
- projectInstalledVersion,
1888
- processPackageMismatch: mismatch,
1889
- processStale: mismatch,
1890
- nextAction: mismatch
1891
- ? 'Restart or retarget the Ark MCP server so process arkgateVersion matches the project install. Prefer project-local CLI (`npx arkgate` / `npx arkgate-check`) until identity is matched and versions align. Multi-checkout users: one expectedRoot per project; never reuse another checkout’s projectId.'
1892
- : projectInstalledVersion == null
1893
- ? 'Project has no resolvable node_modules/arkgate; install the package or use CLI from a project that pins arkgate.'
1894
- : 'Process package version matches project install for this MCP root.',
1895
- };
1913
+ return buildProcessPackageHonesty({
1914
+ processVersion: typeof ark.version === 'string' ? ark.version : null,
1915
+ root: resolvedRoot,
1916
+ });
1896
1917
  }
1897
1918
 
1898
1919
  function contextFor(binding) {
package/bin/ark.mjs CHANGED
@@ -207,8 +207,8 @@ Options:
207
207
  --remove-host <host>
208
208
  Preview or apply removal of that compact host integration; re-add it with --tools <host>.
209
209
  --require-write-hook <host>
210
- Require and verify a hard local write hook for Claude or Grok. Cursor/Codex are
211
- advisory-write plus hard CI merge only; impossible requests fail before any write.
210
+ Require and verify a hard local write hook for Claude, Grok, Antigravity, or Cursor.
211
+ Codex/OpenCode are advisory-write plus hard CI merge only; impossible requests fail before any write.
212
212
 
213
213
  Interactive mode (TTY, no --yes): asks what application shape you are building and maps it to a preset.
214
214
  Non-interactive (no TTY): uses the same defaults as --yes — never calls readline on a null interface.
@@ -26,9 +26,11 @@ export {
26
26
  antigravityHooks,
27
27
  claudeSettings,
28
28
  codexProjectConfig,
29
+ cursorHooks,
29
30
  grokHooks,
30
31
  grokProjectConfig,
31
32
  mergeAntigravityArkHook,
33
+ mergeCursorArkHook,
32
34
  mergeOpencodeArkMcp,
33
35
  opencodeProjectConfig,
34
36
  } from './hook-templates.mjs';
@@ -370,7 +370,8 @@ ${steps}
370
370
  ${placementBody}
371
371
 
372
372
  The project is only considered Ark-enforced when its host-appropriate write path is configured
373
- and the CI check passes. Only Claude/Grok provide a hard local write boundary; Cursor/Codex use
373
+ and the CI check passes. Only Claude/Grok/Antigravity/Cursor provide a hard local write boundary
374
+ (Cursor: Write/StrReplace when .cursor/hooks.json is trusted); Codex/OpenCode use
374
375
  advisory MCP plus CI. The experimental runtime is not required.
375
376
  `;
376
377
  }
@@ -127,7 +127,7 @@ const FAIL_CLOSED_ARK_FLAG = /(?:^|\s)--(?:strict|strict-merge|require-gates)(?=
127
127
  const COMPACT_HOST_FILES = {
128
128
  claude: ['.claude/settings.json'],
129
129
  grok: ['.grok/config.toml', '.grok/hooks/ark-write-gate.json'],
130
- cursor: ['.cursor/mcp.json'],
130
+ cursor: ['.cursor/mcp.json', '.cursor/hooks.json'],
131
131
  codex: ['.codex/hooks.json', '.codex/config.toml'],
132
132
  windsurf: ['.windsurf/rules/ark.md'],
133
133
  cline: ['.clinerules/ark.md'],
@@ -1,20 +1,22 @@
1
1
  // Generated from hook-templates.source.mjs — run npm run generate:packaged-tooling.
2
- import{execCommandParts as i,execRunner as s}from"../ark-shared.mjs";const c="arkgate-mcp";function l(o){const r=s(o);return`${JSON.stringify({hooks:{SessionStart:[{hooks:[{type:"command",command:`${r} ${c} --session-context --root . --root-env CLAUDE_PROJECT_DIR --config ark.config.json`}]}],PreToolUse:[{matcher:"Write|Edit|MultiEdit",hooks:[{type:"command",command:`${r} ${c} --hook --hook-repair --fail-on-new-smells --root . --root-env CLAUDE_PROJECT_DIR --config ark.config.json`}]}]}},null,2)}
3
- `}function p(o){const r=s(o);return`${JSON.stringify({hooks:{SessionStart:[{hooks:[{type:"command",timeout:30,command:`${r} ${c} --session-context --root . --root-env CODEX_PROJECT_DIR --config ark.config.json`}]}],PreToolUse:[{matcher:"ApplyPatch|apply_patch|Write|Edit|MultiEdit",hooks:[{type:"command",timeout:30,command:`${r} ${c} --hook --hook-repair --fail-on-new-smells --root . --root-env CODEX_PROJECT_DIR --config ark.config.json`}]}]}},null,2)}
4
- `}function f(o){const{command:r,args:e}=i(o,c,["--root",".","--config","ark.config.json"]),t=a=>a.replace(/\\/g,"\\\\").replace(/"/g,'\\"'),n=e.map(a=>`"${t(a)}"`).join(", ");return`# Generated by ark-check --install-agent-gates (Codex project scope).
2
+ import{execCommandParts as l,execRunner as i}from"../ark-shared.mjs";const s="arkgate-mcp";function h(e){const r=i(e);return`${JSON.stringify({version:1,hooks:{preToolUse:[{command:`${r} ${s} --hook --hook-repair --fail-on-new-smells --root . --root-env CURSOR_PROJECT_DIR --config ark.config.json`,matcher:"Write|StrReplace",failClosed:!0,timeout:30}]}},null,2)}
3
+ `}function y(e){const r=i(e);return`${JSON.stringify({hooks:{SessionStart:[{hooks:[{type:"command",command:`${r} ${s} --session-context --root . --root-env CLAUDE_PROJECT_DIR --config ark.config.json`}]}],PreToolUse:[{matcher:"Write|Edit|MultiEdit",hooks:[{type:"command",command:`${r} ${s} --hook --hook-repair --fail-on-new-smells --root . --root-env CLAUDE_PROJECT_DIR --config ark.config.json`}]}]}},null,2)}
4
+ `}function $(e){const r=i(e);return`${JSON.stringify({hooks:{SessionStart:[{hooks:[{type:"command",timeout:30,command:`${r} ${s} --session-context --root . --root-env CODEX_PROJECT_DIR --config ark.config.json`}]}],PreToolUse:[{matcher:"ApplyPatch|apply_patch|Write|Edit|MultiEdit",hooks:[{type:"command",timeout:30,command:`${r} ${s} --hook --hook-repair --fail-on-new-smells --root . --root-env CODEX_PROJECT_DIR --config ark.config.json`}]}]}},null,2)}
5
+ `}function O(e){const{command:r,args:o}=l(e,s,["--root",".","--config","ark.config.json"]),t=a=>a.replace(/\\/g,"\\\\").replace(/"/g,'\\"'),c=o.map(a=>`"${t(a)}"`).join(", ");return`# Generated by ark-check --install-agent-gates (Codex project scope).
5
6
  # CONFIGURED ON DISK \u2014 RUNTIME NOT VERIFIED.
6
7
  # Restart Codex, then call ark_identity with expectedRoot before trusting MCP verdicts.
7
8
  [mcp_servers.ark]
8
9
  command = "${t(r)}"
9
- args = [${n}]
10
- `}function g(o){const{command:r,args:e}=i(o,c,["--root",".","--config","ark.config.json"]),t=e.map(n=>`"${n.replace(/\\/g,"\\\\").replace(/"/g,'\\"')}"`).join(", ");return`# Generated by ark-check --install-agent-gates (Grok Build project scope).
10
+ args = [${c}]
11
+ `}function R(e){const{command:r,args:o}=l(e,s,["--root",".","--config","ark.config.json"]),t=o.map(c=>`"${c.replace(/\\/g,"\\\\").replace(/"/g,'\\"')}"`).join(", ");return`# Generated by ark-check --install-agent-gates (Grok Build project scope).
11
12
  # Restart Grok (or /mcps \u2192 refresh) after changes. Also loads repo-root .mcp.json.
12
13
  [mcp_servers.ark]
13
14
  command = "${r.replace(/\\/g,"\\\\").replace(/"/g,'\\"')}"
14
15
  args = [${t}]
15
- `}function u(o){const r=s(o);return`${JSON.stringify({hooks:{SessionStart:[{hooks:[{type:"command",timeout:30,command:`${r} ${c} --session-context --root . --root-env GROK_WORKSPACE_ROOT,CLAUDE_PROJECT_DIR --config ark.config.json`}]}],PreToolUse:[{matcher:"Write|Edit|MultiEdit|write|search_replace",hooks:[{type:"command",timeout:30,command:`${r} ${c} --hook --hook-repair --fail-on-new-smells --root . --root-env GROK_WORKSPACE_ROOT,CLAUDE_PROJECT_DIR --config ark.config.json`}]}]}},null,2)}
16
- `}function k(o){const r=s(o);return`${JSON.stringify({"ark-write-gate":{PreToolUse:[{matcher:"write_to_file|replace_file_content|multi_replace_file_content",hooks:[{type:"command",timeout:30,command:`${r} ${c} --hook --hook-repair --fail-on-new-smells --root . --config ark.config.json`}]}]}},null,2)}
17
- `}function d(o){const{command:r,args:e}=i(o,c,["--root",".","--config","ark.config.json"]);return`${JSON.stringify({$schema:"https://opencode.ai/config.json",mcp:{ark:{type:"local",command:[r,...e],enabled:!0}}},null,2)}
18
- `}function h(o,r){let e,t;try{e=o&&o.trim()?JSON.parse(o):{},t=JSON.parse(r)}catch{return null}if(!e||typeof e!="object"||Array.isArray(e)||!t||typeof t!="object"||Array.isArray(t))return null;const n=t["ark-write-gate"];if(!n||typeof n!="object")return null;const a={...e,"ark-write-gate":n};return`${JSON.stringify(a,null,2)}
19
- `}function y(o,r){let e,t;try{e=o&&o.trim()?JSON.parse(o):{},t=JSON.parse(r)}catch{return null}if(!e||typeof e!="object"||Array.isArray(e))return null;const n={...e};!n.$schema&&t.$schema&&(n.$schema=t.$schema);const a=e.mcp&&typeof e.mcp=="object"&&!Array.isArray(e.mcp)?{...e.mcp}:{};return a.ark=t.mcp.ark,n.mcp=a,`${JSON.stringify(n,null,2)}
20
- `}export{c as PREFERRED_MCP_BIN,k as antigravityHooks,l as claudeSettings,p as codexHooks,f as codexProjectConfig,u as grokHooks,g as grokProjectConfig,h as mergeAntigravityArkHook,y as mergeOpencodeArkMcp,d as opencodeProjectConfig};
16
+ `}function _(e){const r=i(e);return`${JSON.stringify({hooks:{SessionStart:[{hooks:[{type:"command",timeout:30,command:`${r} ${s} --session-context --root . --root-env GROK_WORKSPACE_ROOT,CLAUDE_PROJECT_DIR --config ark.config.json`}]}],PreToolUse:[{matcher:"Write|Edit|MultiEdit|write|search_replace",hooks:[{type:"command",timeout:30,command:`${r} ${s} --hook --hook-repair --fail-on-new-smells --root . --root-env GROK_WORKSPACE_ROOT,CLAUDE_PROJECT_DIR --config ark.config.json`}]}]}},null,2)}
17
+ `}function A(e){const r=i(e);return`${JSON.stringify({"ark-write-gate":{PreToolUse:[{matcher:"write_to_file|replace_file_content|multi_replace_file_content",hooks:[{type:"command",timeout:30,command:`${r} ${s} --hook --hook-repair --fail-on-new-smells --root . --config ark.config.json`}]}]}},null,2)}
18
+ `}function j(e){const{command:r,args:o}=l(e,s,["--root",".","--config","ark.config.json"]);return`${JSON.stringify({$schema:"https://opencode.ai/config.json",mcp:{ark:{type:"local",command:[r,...o],enabled:!0}}},null,2)}
19
+ `}function E(e,r){let o,t;try{o=e&&e.trim()?JSON.parse(e):{},t=JSON.parse(r)}catch{return null}if(!o||typeof o!="object"||Array.isArray(o)||!t||typeof t!="object"||Array.isArray(t))return null;const c=t.hooks,p=(Array.isArray(c?.preToolUse)?c.preToolUse:[]).find(n=>n&&typeof n=="object"&&typeof n.command=="string"&&/arkgate-mcp|ark-mcp/.test(n.command)&&/\s--hook(?:\s|$)/.test(` ${n.command} `));if(!p)return null;const m=o.hooks&&typeof o.hooks=="object"&&!Array.isArray(o.hooks)?{...o.hooks}:{},f=Array.isArray(m.preToolUse)?[...m.preToolUse]:[],k=n=>n&&typeof n=="object"&&typeof n.command=="string"&&/arkgate-mcp|ark-mcp/.test(n.command)&&/\s--hook(?:\s|$)/.test(` ${n.command} `)&&(typeof n.matcher!="string"||/Write/.test(n.matcher)||/StrReplace/.test(n.matcher)),g=f.filter(n=>!k(n));m.preToolUse=[...g,p];const u={...o,version:o.version??t.version??1,hooks:m};return`${JSON.stringify(u,null,2)}
20
+ `}function S(e,r){let o,t;try{o=e&&e.trim()?JSON.parse(e):{},t=JSON.parse(r)}catch{return null}if(!o||typeof o!="object"||Array.isArray(o)||!t||typeof t!="object"||Array.isArray(t))return null;const c=t["ark-write-gate"];if(!c||typeof c!="object")return null;const a={...o,"ark-write-gate":c};return`${JSON.stringify(a,null,2)}
21
+ `}function C(e,r){let o,t;try{o=e&&e.trim()?JSON.parse(e):{},t=JSON.parse(r)}catch{return null}if(!o||typeof o!="object"||Array.isArray(o))return null;const c={...o};!c.$schema&&t.$schema&&(c.$schema=t.$schema);const a=o.mcp&&typeof o.mcp=="object"&&!Array.isArray(o.mcp)?{...o.mcp}:{};return a.ark=t.mcp.ark,c.mcp=a,`${JSON.stringify(c,null,2)}
22
+ `}export{s as PREFERRED_MCP_BIN,A as antigravityHooks,y as claudeSettings,$ as codexHooks,O as codexProjectConfig,h as cursorHooks,_ as grokHooks,R as grokProjectConfig,S as mergeAntigravityArkHook,E as mergeCursorArkHook,C as mergeOpencodeArkMcp,j as opencodeProjectConfig};
@@ -70,9 +70,27 @@ export const HOST_SUPPORT_MATRIX = Object.freeze({
70
70
  true,
71
71
  true
72
72
  ),
73
- cursor: hostProfile('Cursor', null, null, [], false, false, {
74
- operationCoverage: { shell: false, 'pre-commit': false },
75
- }),
73
+ // Cursor: official preToolUse deny / exit 2 is a hard block for matched tools when
74
+ // `.cursor/hooks.json` is installed + trusted. Claim hard only for Write|StrReplace;
75
+ // Shell/Tab/human edits still rely on CI. Repair envelope may emit; Write updated_input
76
+ // reinjection is not guaranteed on Cursor (agent_message + retry is the supported path).
77
+ cursor: hostProfile(
78
+ 'Cursor',
79
+ '.cursor/hooks.json',
80
+ 'preToolUse `Write` / `StrReplace`',
81
+ ['Write', 'StrReplace'],
82
+ true,
83
+ false,
84
+ {
85
+ repairEnvelopeEmitted: true,
86
+ operationCoverage: {
87
+ Write: true,
88
+ StrReplace: true,
89
+ shell: false,
90
+ 'pre-commit': false,
91
+ },
92
+ }
93
+ ),
76
94
  codex: hostProfile(
77
95
  'OpenAI Codex',
78
96
  '.codex/hooks.json',
@@ -134,14 +152,15 @@ export function renderHostSupportMatrixMarkdown() {
134
152
  const rows = HOST_SUPPORT_HOSTS.map((host) => {
135
153
  const profile = HOST_SUPPORT_MATRIX[host];
136
154
  const capabilities = profile.capabilities;
137
- // Fail-closed honesty: Cursor/Codex/OpenCode never claim hard write; CI is required-status.
138
- // hookSurface already includes "PreToolUse …" do not prefix PreToolUse again.
155
+ // Fail-closed honesty: Codex/OpenCode never claim hard write; CI is required-status.
156
+ // Cursor claims hard only for listed preToolUse ops when hooks are installed + trusted.
157
+ // hookSurface already includes "PreToolUse/preToolUse …" — do not prefix again.
139
158
  let local;
140
159
  if (capabilities['hard-write']) {
141
160
  local = `**Hard** block for listed ops (${profile.hookSurface}) when installed + trusted`;
142
161
  } else if (host === 'codex') {
143
162
  local =
144
- '**Advisory / best-effort** at write (not equivalent to Claude/Grok hard block)';
163
+ '**Advisory / best-effort** at write (not equivalent to Claude/Grok/Cursor hard block)';
145
164
  } else if (host === 'opencode') {
146
165
  local =
147
166
  '**Advisory / best-effort** at write (MCP + optional plugin; not a hard boundary)';
@@ -169,7 +188,8 @@ ${rows}
169
188
 
170
189
  **Read the CI column:** for every host, the repository-wide hard guarantee is a **required**
171
190
  GitHub **status context** that runs the CLI — not “CI file present,” and not the CLI binary name alone.
172
- Cursor/Codex/OpenCode never get a fake hard write claim.
191
+ Codex/OpenCode never get a fake hard write claim. Cursor hard write covers only listed
192
+ \`preToolUse\` ops when \`.cursor/hooks.json\` is installed and trusted — Shell/Tab/human edits still rely on CI.
173
193
 
174
194
  This table describes the supported profile **after its files are installed and the host loads/trusts them**. A hard local boundary covers only the listed hook operations; alternate tools, direct filesystem writes, and human edits still rely on CI. MCP validation is advisory because the agent must call it. The CI check blocks a merge only when the repository makes that status required. Repair **envelopes** may be emitted without reinjection being guaranteed; silent auto-apply never happens. Run \`arkgate-check --doctor\` (or \`ark-check --doctor\`) for the evidence actually detected in the current repository.`;
175
195
  }
@@ -203,14 +223,14 @@ export function doctorWritePathHonestyMessage(activeHost, hardWriteActive) {
203
223
  // EH07: distinguish CLI command (arkgate-check / ark-check) from the GitHub required status context name.
204
224
  const mergeBoundary =
205
225
  'Required CI hard merge boundary = a required GitHub status context that runs arkgate-check --strict-merge (alias ark-check --strict-merge)';
206
- if (host === 'cursor') {
207
- return `Cursor: write path is advisory (MCP/rules; no hard PreToolUse). ${mergeBoundary}.`;
226
+ if (host === 'cursor' && !hardWriteActive) {
227
+ return `Cursor: hard preToolUse is supported for Write/StrReplace when .cursor/hooks.json is installed + trusted; without runtime-observed hook evidence, hard is unverified. ${mergeBoundary}.`;
208
228
  }
209
229
  if (host === 'codex') {
210
- return `Codex: write path is advisory / best-effort at write (not Claude/Grok hard). ${mergeBoundary}.`;
230
+ return `Codex: write path is advisory / best-effort at write (not Claude/Grok/Cursor hard). ${mergeBoundary}.`;
211
231
  }
212
232
  if (host === 'opencode') {
213
- return `OpenCode: write path is advisory / best-effort (MCP + optional plugin; not Claude/Grok/Antigravity hard). ${mergeBoundary}.`;
233
+ return `OpenCode: write path is advisory / best-effort (MCP + optional plugin; not Claude/Grok/Antigravity/Cursor hard). ${mergeBoundary}.`;
214
234
  }
215
235
  if ((host === 'claude' || host === 'grok' || host === 'antigravity') && !hardWriteActive) {
216
236
  const label =
@@ -25,9 +25,11 @@ import {
25
25
  claudeSettings,
26
26
  codexHooks,
27
27
  codexProjectConfig,
28
+ cursorHooks,
28
29
  grokHooks,
29
30
  grokProjectConfig,
30
31
  mergeAntigravityArkHook,
32
+ mergeCursorArkHook,
31
33
  mergeOpencodeArkMcp,
32
34
  opencodeProjectConfig,
33
35
  } from './hook-templates.mjs';
@@ -194,6 +196,9 @@ export function buildManagedAssetCatalog({ root, tools, compact = false, skillsO
194
196
  );
195
197
  if (selectedTools.has('cursor')) {
196
198
  add('.cursor/mcp.json', mcpJson(root));
199
+ // whole-file for managed upgrade/manifest (json-merge is install-time only via
200
+ // mergeCursorArkHook below — same pattern as Antigravity hooks).
201
+ add('.cursor/hooks.json', cursorHooks(root));
197
202
  if (!compact) add('.cursor/rules/ark.mdc', cursorRule(root));
198
203
  }
199
204
  if (selectedTools.has('claude')) add('.claude/settings.json', claudeSettings(root));
@@ -516,6 +521,25 @@ export function runInstallAgentGates(args) {
516
521
  if (merged === existing) return { relativePath, status: 'skipped' };
517
522
  return writeTemplate(root, relativePath, merged, true);
518
523
  }
524
+ if (relativePath === '.cursor/hooks.json') {
525
+ const fullPath = path.join(root, relativePath);
526
+ let existing = '';
527
+ try {
528
+ existing = fs.readFileSync(fullPath, 'utf8');
529
+ } catch {
530
+ // Missing hooks file → write generated Cursor preToolUse gate.
531
+ }
532
+ if (!existing) {
533
+ return writeTemplate(root, relativePath, content, true);
534
+ }
535
+ const merged = mergeCursorArkHook(existing, content);
536
+ if (merged == null) {
537
+ return { relativePath, status: 'skipped-non-ark' };
538
+ }
539
+ if (merged === existing) return { relativePath, status: 'skipped' };
540
+ // Upsert Ark preToolUse without requiring --force; never wipe sibling hooks.
541
+ return writeTemplate(root, relativePath, merged, true);
542
+ }
519
543
  if (relativePath === '.agents/hooks.json') {
520
544
  const fullPath = path.join(root, relativePath);
521
545
  let existing = '';
@@ -126,7 +126,12 @@ function hasArkText(root, relativePath) {
126
126
 
127
127
  const HOST_SIGNALS = {
128
128
  claude: ['.claude/settings.json', '.claude/skills/ark-upgrade/SKILL.md'],
129
- cursor: ['.cursor/mcp.json', '.cursor/rules/ark.mdc', '.cursor/commands/ark-upgrade.md'],
129
+ cursor: [
130
+ '.cursor/mcp.json',
131
+ '.cursor/hooks.json',
132
+ '.cursor/rules/ark.mdc',
133
+ '.cursor/commands/ark-upgrade.md',
134
+ ],
130
135
  codex: ['.codex/hooks.json', '.codex/config.toml', '.agents/skills/ark-upgrade/SKILL.md'],
131
136
  grok: ['.grok/config.toml', '.grok/hooks/ark-write-gate.json', '.grok/skills/ark-upgrade/SKILL.md'],
132
137
  antigravity: ['.agents/hooks.json', '.agents/skills/ark-upgrade/SKILL.md'],