@intutic/sync-daemon 2.1.0 → 2.3.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.
Files changed (288) hide show
  1. package/README.md +1 -1
  2. package/dist/agentReporter.d.ts +24 -11
  3. package/dist/agentReporter.d.ts.map +1 -1
  4. package/dist/agentReporter.js +20 -13
  5. package/dist/agentReporter.js.map +1 -1
  6. package/dist/configReader.d.ts +45 -25
  7. package/dist/configReader.d.ts.map +1 -1
  8. package/dist/configReader.js +70 -46
  9. package/dist/configReader.js.map +1 -1
  10. package/dist/configWriter.d.ts +32 -45
  11. package/dist/configWriter.d.ts.map +1 -1
  12. package/dist/configWriter.js +52 -686
  13. package/dist/configWriter.js.map +1 -1
  14. package/dist/disconnect/harnesses.d.ts +29 -0
  15. package/dist/disconnect/harnesses.d.ts.map +1 -0
  16. package/dist/disconnect/harnesses.js +805 -0
  17. package/dist/disconnect/harnesses.js.map +1 -0
  18. package/dist/disconnect/index.d.ts +25 -0
  19. package/dist/disconnect/index.d.ts.map +1 -0
  20. package/dist/disconnect/index.js +92 -0
  21. package/dist/disconnect/index.js.map +1 -0
  22. package/dist/disconnect/mcp.d.ts +81 -0
  23. package/dist/disconnect/mcp.d.ts.map +1 -0
  24. package/dist/disconnect/mcp.js +354 -0
  25. package/dist/disconnect/mcp.js.map +1 -0
  26. package/dist/disconnect/originals.d.ts +88 -0
  27. package/dist/disconnect/originals.d.ts.map +1 -0
  28. package/dist/disconnect/originals.js +257 -0
  29. package/dist/disconnect/originals.js.map +1 -0
  30. package/dist/disconnect/plan.d.ts +175 -0
  31. package/dist/disconnect/plan.d.ts.map +1 -0
  32. package/dist/disconnect/plan.js +580 -0
  33. package/dist/disconnect/plan.js.map +1 -0
  34. package/dist/disconnect/recognise.d.ts +43 -0
  35. package/dist/disconnect/recognise.d.ts.map +1 -0
  36. package/dist/disconnect/recognise.js +108 -0
  37. package/dist/disconnect/recognise.js.map +1 -0
  38. package/dist/disconnect/tomlLines.d.ts +39 -0
  39. package/dist/disconnect/tomlLines.d.ts.map +1 -0
  40. package/dist/disconnect/tomlLines.js +105 -0
  41. package/dist/disconnect/tomlLines.js.map +1 -0
  42. package/dist/harness/aiderConfigMerger.d.ts +29 -8
  43. package/dist/harness/aiderConfigMerger.d.ts.map +1 -1
  44. package/dist/harness/aiderConfigMerger.js +63 -12
  45. package/dist/harness/aiderConfigMerger.js.map +1 -1
  46. package/dist/harness/antigravityCliHooks.d.ts +61 -0
  47. package/dist/harness/antigravityCliHooks.d.ts.map +1 -0
  48. package/dist/harness/antigravityCliHooks.js +207 -0
  49. package/dist/harness/antigravityCliHooks.js.map +1 -0
  50. package/dist/harness/antigravityHooks.d.ts +12 -2
  51. package/dist/harness/antigravityHooks.d.ts.map +1 -1
  52. package/dist/harness/antigravityHooks.js +38 -16
  53. package/dist/harness/antigravityHooks.js.map +1 -1
  54. package/dist/harness/antigravityProducts.d.ts +40 -0
  55. package/dist/harness/antigravityProducts.d.ts.map +1 -0
  56. package/dist/harness/antigravityProducts.js +109 -0
  57. package/dist/harness/antigravityProducts.js.map +1 -0
  58. package/dist/harness/claudeAgentsMd.d.ts +29 -0
  59. package/dist/harness/claudeAgentsMd.d.ts.map +1 -0
  60. package/dist/harness/claudeAgentsMd.js +80 -0
  61. package/dist/harness/claudeAgentsMd.js.map +1 -0
  62. package/dist/harness/claudeCodeHooks.d.ts +6 -6
  63. package/dist/harness/claudeCodeHooks.d.ts.map +1 -1
  64. package/dist/harness/claudeCodeHooks.js +115 -28
  65. package/dist/harness/claudeCodeHooks.js.map +1 -1
  66. package/dist/harness/claudeProjectApproval.d.ts +53 -0
  67. package/dist/harness/claudeProjectApproval.d.ts.map +1 -0
  68. package/dist/harness/claudeProjectApproval.js +159 -0
  69. package/dist/harness/claudeProjectApproval.js.map +1 -0
  70. package/dist/harness/clineHooks.d.ts +0 -1
  71. package/dist/harness/clineHooks.d.ts.map +1 -1
  72. package/dist/harness/clineHooks.js +13 -4
  73. package/dist/harness/clineHooks.js.map +1 -1
  74. package/dist/harness/codexConfigMerger.d.ts.map +1 -1
  75. package/dist/harness/codexConfigMerger.js +4 -0
  76. package/dist/harness/codexConfigMerger.js.map +1 -1
  77. package/dist/harness/codexHooks.d.ts +2 -0
  78. package/dist/harness/codexHooks.d.ts.map +1 -1
  79. package/dist/harness/codexHooks.js +15 -4
  80. package/dist/harness/codexHooks.js.map +1 -1
  81. package/dist/harness/continueConfigMerger.d.ts.map +1 -1
  82. package/dist/harness/continueConfigMerger.js +4 -0
  83. package/dist/harness/continueConfigMerger.js.map +1 -1
  84. package/dist/harness/cursorHooks.d.ts +5 -2
  85. package/dist/harness/cursorHooks.d.ts.map +1 -1
  86. package/dist/harness/cursorHooks.js +12 -3
  87. package/dist/harness/cursorHooks.js.map +1 -1
  88. package/dist/harness/dshHooks.d.ts +5 -5
  89. package/dist/harness/dshHooks.d.ts.map +1 -1
  90. package/dist/harness/dshHooks.js +12 -5
  91. package/dist/harness/dshHooks.js.map +1 -1
  92. package/dist/harness/gateArtifacts.d.ts +35 -0
  93. package/dist/harness/gateArtifacts.d.ts.map +1 -0
  94. package/dist/harness/gateArtifacts.js +88 -0
  95. package/dist/harness/gateArtifacts.js.map +1 -0
  96. package/dist/harness/gateBody.d.ts +195 -69
  97. package/dist/harness/gateBody.d.ts.map +1 -1
  98. package/dist/harness/gateBody.js +847 -182
  99. package/dist/harness/gateBody.js.map +1 -1
  100. package/dist/harness/gateSightings.d.ts +47 -0
  101. package/dist/harness/gateSightings.d.ts.map +1 -0
  102. package/dist/harness/gateSightings.js +159 -0
  103. package/dist/harness/gateSightings.js.map +1 -0
  104. package/dist/harness/githubCopilotHooks.d.ts.map +1 -1
  105. package/dist/harness/githubCopilotHooks.js +11 -10
  106. package/dist/harness/githubCopilotHooks.js.map +1 -1
  107. package/dist/harness/gooseHardener.d.ts +0 -1
  108. package/dist/harness/gooseHardener.d.ts.map +1 -1
  109. package/dist/harness/gooseHardener.js +1 -1
  110. package/dist/harness/gooseHardener.js.map +1 -1
  111. package/dist/harness/gooseHooks.d.ts +29 -1
  112. package/dist/harness/gooseHooks.d.ts.map +1 -1
  113. package/dist/harness/gooseHooks.js +107 -69
  114. package/dist/harness/gooseHooks.js.map +1 -1
  115. package/dist/harness/grokHooks.d.ts +2 -2
  116. package/dist/harness/grokHooks.d.ts.map +1 -1
  117. package/dist/harness/grokHooks.js +20 -18
  118. package/dist/harness/grokHooks.js.map +1 -1
  119. package/dist/harness/hermesHooks.d.ts +18 -8
  120. package/dist/harness/hermesHooks.d.ts.map +1 -1
  121. package/dist/harness/hermesHooks.js +55 -19
  122. package/dist/harness/hermesHooks.js.map +1 -1
  123. package/dist/harness/holdRedaction.d.ts +4 -1
  124. package/dist/harness/holdRedaction.d.ts.map +1 -1
  125. package/dist/harness/holdRedaction.js +32 -8
  126. package/dist/harness/holdRedaction.js.map +1 -1
  127. package/dist/harness/jetbrainsXmlConfig.d.ts +20 -0
  128. package/dist/harness/jetbrainsXmlConfig.d.ts.map +1 -1
  129. package/dist/harness/jetbrainsXmlConfig.js +39 -18
  130. package/dist/harness/jetbrainsXmlConfig.js.map +1 -1
  131. package/dist/harness/mcpAutoWrite.d.ts +62 -25
  132. package/dist/harness/mcpAutoWrite.d.ts.map +1 -1
  133. package/dist/harness/mcpAutoWrite.js +406 -95
  134. package/dist/harness/mcpAutoWrite.js.map +1 -1
  135. package/dist/harness/museHooks.d.ts +3 -3
  136. package/dist/harness/museHooks.d.ts.map +1 -1
  137. package/dist/harness/museHooks.js +15 -7
  138. package/dist/harness/museHooks.js.map +1 -1
  139. package/dist/harness/n8nHooks.d.ts +0 -1
  140. package/dist/harness/n8nHooks.d.ts.map +1 -1
  141. package/dist/harness/n8nHooks.js +4 -1
  142. package/dist/harness/n8nHooks.js.map +1 -1
  143. package/dist/harness/openCodeHooks.d.ts +2 -2
  144. package/dist/harness/openCodeHooks.d.ts.map +1 -1
  145. package/dist/harness/openCodeHooks.js +9 -8
  146. package/dist/harness/openCodeHooks.js.map +1 -1
  147. package/dist/harness/openWebuiHooks.d.ts +0 -1
  148. package/dist/harness/openWebuiHooks.d.ts.map +1 -1
  149. package/dist/harness/openWebuiHooks.js +11 -3
  150. package/dist/harness/openWebuiHooks.js.map +1 -1
  151. package/dist/harness/openclawHooks.d.ts +75 -18
  152. package/dist/harness/openclawHooks.d.ts.map +1 -1
  153. package/dist/harness/openclawHooks.js +214 -160
  154. package/dist/harness/openclawHooks.js.map +1 -1
  155. package/dist/harness/openhandsHooks.d.ts +37 -11
  156. package/dist/harness/openhandsHooks.d.ts.map +1 -1
  157. package/dist/harness/openhandsHooks.js +148 -97
  158. package/dist/harness/openhandsHooks.js.map +1 -1
  159. package/dist/harness/piHooks.d.ts +52 -17
  160. package/dist/harness/piHooks.d.ts.map +1 -1
  161. package/dist/harness/piHooks.js +177 -163
  162. package/dist/harness/piHooks.js.map +1 -1
  163. package/dist/harness/protectedPaths.d.ts +107 -25
  164. package/dist/harness/protectedPaths.d.ts.map +1 -1
  165. package/dist/harness/protectedPaths.js +342 -48
  166. package/dist/harness/protectedPaths.js.map +1 -1
  167. package/dist/harness/rulesSection.d.ts +62 -0
  168. package/dist/harness/rulesSection.d.ts.map +1 -0
  169. package/dist/harness/rulesSection.js +145 -0
  170. package/dist/harness/rulesSection.js.map +1 -0
  171. package/dist/harness/windsurfHooks.d.ts +18 -3
  172. package/dist/harness/windsurfHooks.d.ts.map +1 -1
  173. package/dist/harness/windsurfHooks.js +41 -8
  174. package/dist/harness/windsurfHooks.js.map +1 -1
  175. package/dist/harness/windsurfJetBrainsProxy.d.ts +1 -1
  176. package/dist/harness/windsurfJetBrainsProxy.d.ts.map +1 -1
  177. package/dist/harness/windsurfJetBrainsProxy.js +5 -1
  178. package/dist/harness/windsurfJetBrainsProxy.js.map +1 -1
  179. package/dist/index.d.ts +21 -20
  180. package/dist/index.d.ts.map +1 -1
  181. package/dist/index.js +19 -19
  182. package/dist/index.js.map +1 -1
  183. package/dist/inventory.d.ts +53 -0
  184. package/dist/inventory.d.ts.map +1 -0
  185. package/dist/inventory.js +133 -0
  186. package/dist/inventory.js.map +1 -0
  187. package/dist/lib/complianceProbes.d.ts +2 -1
  188. package/dist/lib/complianceProbes.d.ts.map +1 -1
  189. package/dist/lib/complianceProbes.js +1 -1
  190. package/dist/lib/complianceProbes.js.map +1 -1
  191. package/dist/lib/decisionsDigest.d.ts +35 -17
  192. package/dist/lib/decisionsDigest.d.ts.map +1 -1
  193. package/dist/lib/decisionsDigest.js +106 -47
  194. package/dist/lib/decisionsDigest.js.map +1 -1
  195. package/dist/lib/egressPolicy.d.ts +2 -2
  196. package/dist/lib/egressPolicy.js +2 -2
  197. package/dist/lib/mcpRegistryPy.d.ts +11 -0
  198. package/dist/lib/mcpRegistryPy.d.ts.map +1 -0
  199. package/dist/lib/mcpRegistryPy.js +173 -0
  200. package/dist/lib/mcpRegistryPy.js.map +1 -0
  201. package/dist/lib/phrasesPy.d.ts +11 -0
  202. package/dist/lib/phrasesPy.d.ts.map +1 -0
  203. package/dist/lib/phrasesPy.js +351 -0
  204. package/dist/lib/phrasesPy.js.map +1 -0
  205. package/dist/lib/policySnapshot.d.ts +63 -9
  206. package/dist/lib/policySnapshot.d.ts.map +1 -1
  207. package/dist/lib/policySnapshot.js +269 -45
  208. package/dist/lib/policySnapshot.js.map +1 -1
  209. package/dist/lib/processPoller.d.ts.map +1 -1
  210. package/dist/lib/processPoller.js +9 -2
  211. package/dist/lib/processPoller.js.map +1 -1
  212. package/dist/lib/regexLinearity.d.ts +43 -0
  213. package/dist/lib/regexLinearity.d.ts.map +1 -0
  214. package/dist/lib/regexLinearity.js +275 -0
  215. package/dist/lib/regexLinearity.js.map +1 -0
  216. package/dist/lib/runtimeEnv.d.ts +1 -3
  217. package/dist/lib/runtimeEnv.d.ts.map +1 -1
  218. package/dist/lib/runtimeEnv.js +0 -2
  219. package/dist/lib/runtimeEnv.js.map +1 -1
  220. package/dist/lib/sequencePy.d.ts +10 -0
  221. package/dist/lib/sequencePy.d.ts.map +1 -0
  222. package/dist/lib/sequencePy.js +102 -0
  223. package/dist/lib/sequencePy.js.map +1 -0
  224. package/dist/sessionReporter.d.ts +18 -12
  225. package/dist/sessionReporter.d.ts.map +1 -1
  226. package/dist/sessionReporter.js +37 -24
  227. package/dist/sessionReporter.js.map +1 -1
  228. package/dist/skillWriter.d.ts +23 -5
  229. package/dist/skillWriter.d.ts.map +1 -1
  230. package/dist/skillWriter.js +159 -16
  231. package/dist/skillWriter.js.map +1 -1
  232. package/dist/syncCycle.d.ts +142 -0
  233. package/dist/syncCycle.d.ts.map +1 -0
  234. package/dist/syncCycle.js +327 -0
  235. package/dist/syncCycle.js.map +1 -0
  236. package/dist/trajectoryMonitor.d.ts +2 -4
  237. package/dist/trajectoryMonitor.d.ts.map +1 -1
  238. package/dist/trajectoryMonitor.js +2 -5
  239. package/dist/trajectoryMonitor.js.map +1 -1
  240. package/dist/watcher/driftWatcher.d.ts +0 -1
  241. package/dist/watcher/driftWatcher.d.ts.map +1 -1
  242. package/dist/watcher/driftWatcher.js +1 -2
  243. package/dist/watcher/driftWatcher.js.map +1 -1
  244. package/dist/watcher/settingsGuard.d.ts +36 -7
  245. package/dist/watcher/settingsGuard.d.ts.map +1 -1
  246. package/dist/watcher/settingsGuard.js +397 -60
  247. package/dist/watcher/settingsGuard.js.map +1 -1
  248. package/dist/watcher/vscodeHookSettings.d.ts +30 -0
  249. package/dist/watcher/vscodeHookSettings.d.ts.map +1 -0
  250. package/dist/watcher/vscodeHookSettings.js +170 -0
  251. package/dist/watcher/vscodeHookSettings.js.map +1 -0
  252. package/dist/wsClient.d.ts +0 -1
  253. package/dist/wsClient.d.ts.map +1 -1
  254. package/dist/wsClient.js +0 -1
  255. package/dist/wsClient.js.map +1 -1
  256. package/package.json +5 -5
  257. package/dist/harness/claudeDesktopHooks.d.ts +0 -30
  258. package/dist/harness/claudeDesktopHooks.d.ts.map +0 -1
  259. package/dist/harness/claudeDesktopHooks.js +0 -254
  260. package/dist/harness/claudeDesktopHooks.js.map +0 -1
  261. package/dist/harness/continueHooks.d.ts +0 -63
  262. package/dist/harness/continueHooks.d.ts.map +0 -1
  263. package/dist/harness/continueHooks.js +0 -247
  264. package/dist/harness/continueHooks.js.map +0 -1
  265. package/dist/harness/gateKind.d.ts +0 -164
  266. package/dist/harness/gateKind.d.ts.map +0 -1
  267. package/dist/harness/gateKind.js +0 -144
  268. package/dist/harness/gateKind.js.map +0 -1
  269. package/dist/harness/rooCodeHooks.d.ts +0 -31
  270. package/dist/harness/rooCodeHooks.d.ts.map +0 -1
  271. package/dist/harness/rooCodeHooks.js +0 -286
  272. package/dist/harness/rooCodeHooks.js.map +0 -1
  273. package/dist/hashReporter.d.ts +0 -33
  274. package/dist/hashReporter.d.ts.map +0 -1
  275. package/dist/hashReporter.js +0 -69
  276. package/dist/hashReporter.js.map +0 -1
  277. package/dist/integrityStore.d.ts +0 -35
  278. package/dist/integrityStore.d.ts.map +0 -1
  279. package/dist/integrityStore.js +0 -71
  280. package/dist/integrityStore.js.map +0 -1
  281. package/dist/lib/gitWorktrees.d.ts +0 -140
  282. package/dist/lib/gitWorktrees.d.ts.map +0 -1
  283. package/dist/lib/gitWorktrees.js +0 -191
  284. package/dist/lib/gitWorktrees.js.map +0 -1
  285. package/dist/syncLoop.d.ts +0 -172
  286. package/dist/syncLoop.d.ts.map +0 -1
  287. package/dist/syncLoop.js +0 -878
  288. package/dist/syncLoop.js.map +0 -1
@@ -5,12 +5,19 @@
5
5
  * Called by the drift watcher whenever any protected governance config file changes.
6
6
  *
7
7
  * Behaviour per path type:
8
- * - JSON hook files (Cursor, Cline, OpenHands, Goose): validate marker presence, restore.
8
+ * - Gate files (`GATE_ARTIFACTS`): restored by their writer when deleted or
9
+ * no longer the generated gate.
10
+ * - JSON hook files (Cursor, Windsurf, OpenHands, Muse, Grok): validate marker presence, restore.
11
+ * - Registrations that name the gate script (Codex, GitHub Copilot, Hermes,
12
+ * the Goose config, OpenClaw's openclaw.json): restored while the gate is installed.
9
13
  * - Immutable files (Goose plugin): log governance_override_attempt incident instead of restoring.
10
- * - Text rules (.roorules, .clinerules): restore from last-written hash.
11
- * - Settings files (VS Code, Aider, claude_desktop_config.json): detect drift, restore.
14
+ * - VS Code settings: `chat.useHooks` or a `chat.hookFilesLocations` entry that switches
15
+ * off the GitHub Copilot gate is set back, and nothing else in the file is touched.
16
+ * - The policy snapshot: restored from the last copy the daemon verified when it differs.
17
+ * - Other settings files (claude_desktop_config.json, Grok's TOML): a deletion is logged as drift.
18
+ * - Gemini CLI's ~/.gemini/settings.json and Antigravity's
19
+ * ~/.gemini/config/hooks.json: restore when the gate registration is gone.
12
20
  *
13
- * LLD #14 — settingsGuard.ts
14
21
  * HLD §3.14 — Three-Tier Defense Cascade (Tier 1 Native Gating)
15
22
  *
16
23
  * @module
@@ -22,61 +29,113 @@ import * as crypto from 'node:crypto';
22
29
  import { createLogger } from '@intutic/logger';
23
30
  import { updatePreToolUseHooks, parseSopConstraints } from '../harness/claudeCodeHooks.js';
24
31
  import { writeClineHooks } from '../harness/clineHooks.js';
25
- import { writeCursorHooks } from '../harness/cursorHooks.js';
32
+ import { writeCursorHooks, systemHooksDirFor } from '../harness/cursorHooks.js';
26
33
  import { writeOpenHandsHooks } from '../harness/openhandsHooks.js';
27
34
  import { writeGooseHooks } from '../harness/gooseHooks.js';
28
- import { writeWindsurfHooks } from '../harness/windsurfHooks.js';
35
+ import { writeWindsurfHooks, windsurfSettingsPath } from '../harness/windsurfHooks.js';
29
36
  import { writeMuseHooks } from '../harness/museHooks.js';
30
- import { writeOpenCodeHooks, OPENCODE_PLUGIN_DIR, OPENCODE_PLUGIN_FILE, OPENCODE_PLUGIN_V2_FILE } from '../harness/openCodeHooks.js';
37
+ import { writeOpenCodeHooks } from '../harness/openCodeHooks.js';
31
38
  import { writeGrokHooks } from '../harness/grokHooks.js';
39
+ import { writePiHooks } from '../harness/piHooks.js';
40
+ import { writeOpenclawHooks, OPENCLAW_PLUGIN_FILE } from '../harness/openclawHooks.js';
32
41
  import { writeDshHooks, resolveDshHome, detectDshCoverageGap } from '../harness/dshHooks.js';
42
+ import { writeAntigravityHooks } from '../harness/antigravityHooks.js';
43
+ import { writeAntigravityCliHooks, antigravityHooksPath, ANTIGRAVITY_HOOK_NAME, ANTIGRAVITY_CLI_GATE } from '../harness/antigravityCliHooks.js';
44
+ import { writeCodexHooks, codexUserDir } from '../harness/codexHooks.js';
45
+ import { writeGithubCopilotHooks } from '../harness/githubCopilotHooks.js';
46
+ import { writeHermesHooks } from '../harness/hermesHooks.js';
47
+ import { writeN8nHooks } from '../harness/n8nHooks.js';
48
+ import { writeOpenWebuiHooks } from '../harness/openWebuiHooks.js';
49
+ import { GATE_ARTIFACTS } from '../harness/gateArtifacts.js';
33
50
  import { isImmutable } from '../harness/gooseHardener.js';
51
+ import { readOriginal } from '../disconnect/originals.js';
52
+ import { DEFAULT_SNAPSHOT_DIR, SNAPSHOT_JSON, SNAPSHOT_RULES, restoreVerifiedSnapshot } from '../lib/policySnapshot.js';
53
+ import { repairHookSettings } from './vscodeHookSettings.js';
34
54
  const log = createLogger('sync-settings-guard');
35
55
  /** Marker embedded in every Intutic-generated hook command. */
36
56
  const HOOK_MARKER = '.intutic/hooks/';
57
+ /** Stamped into every gate gateBody.ts emits, whatever its language. */
58
+ const GATE_BODY_MARKER = 'Intutic gate body';
37
59
  const home = os.homedir();
60
+ /** Goose's plugin directory, whose files are hardened rather than restored. */
61
+ const GOOSE_PLUGIN_DIR = path.join(home, '.agents', 'plugins', 'intutic-governance');
62
+ const GOOSE_GATE_SCRIPT = path.join(GOOSE_PLUGIN_DIR, 'scripts', 'intutic-check.sh');
63
+ /** Cursor's machine-wide hooks.json, written by `intutic enterprise install`. */
64
+ const cursorSystemHooks = () => path.join(systemHooksDirFor(process.platform), 'hooks.json');
65
+ /** Harnesses whose writer installs the gate in the home directory rather than the workspace. */
66
+ const PER_USER_GATES = new Set(['goose', 'hermes', 'n8n', 'open-webui', 'openclaw', 'pi']);
67
+ /** Every gate file in {@link GATE_ARTIFACTS}, where its writer puts it. */
68
+ function gateArtifactPaths(workspaceRoot) {
69
+ return Object.entries(GATE_ARTIFACTS).flatMap(([harness, rels]) => (rels ?? []).map((rel) => ({ harness, file: path.join(PER_USER_GATES.has(harness) ? home : workspaceRoot, rel) })));
70
+ }
38
71
  // ─── All protected paths, grouped by harness ─────────────────────────
72
+ /** The live policy snapshot every gate reads, both artifacts. */
73
+ function policySnapshotFiles(snapshotDir = DEFAULT_SNAPSHOT_DIR) {
74
+ return [path.join(snapshotDir, SNAPSHOT_RULES), path.join(snapshotDir, SNAPSHOT_JSON)];
75
+ }
76
+ /** VS Code's user settings (both platforms' locations) and the workspace's. */
77
+ function vscodeSettingsFiles(workspaceRoot) {
78
+ return [
79
+ path.join(home, '.config', 'Code', 'User', 'settings.json'),
80
+ path.join(home, 'Library', 'Application Support', 'Code', 'User', 'settings.json'),
81
+ path.join(workspaceRoot, '.vscode', 'settings.json'),
82
+ ];
83
+ }
39
84
  /** Returns the full list of protected paths to watch. */
40
85
  export function buildProtectedPaths(workspaceRoot) {
41
86
  return [
87
+ // ── Every harness's gate file ────────────────────────────────────
88
+ ...gateArtifactPaths(workspaceRoot).map((g) => g.file),
89
+ // ── The policy snapshot every gate reads ─────────────────────────
90
+ ...policySnapshotFiles(),
42
91
  // ── Claude Code ──────────────────────────────────────────────────
43
92
  path.join(home, '.claude', 'settings.json'),
44
93
  path.join(workspaceRoot, '.claude', 'settings.json'),
45
94
  // ── Cursor (3 levels) ────────────────────────────────────────────
46
- '/etc/cursor/hooks.json',
95
+ cursorSystemHooks(),
47
96
  path.join(home, '.cursor', 'hooks.json'),
48
97
  path.join(workspaceRoot, '.cursor', 'hooks.json'),
49
98
  // ── Windsurf ─────────────────────────────────────────────────────
50
99
  path.join(home, '.codeium', 'windsurf', 'hooks.json'),
51
- path.join(home, '.codeium', 'windsurf', 'settings.json'),
100
+ windsurfSettingsPath(),
52
101
  // JetBrains plugin's separate user-level path (no `windsurf`
53
102
  // subdirectory) — see windsurfHooks.ts's module doc comment.
54
103
  path.join(home, '.codeium', 'hooks.json'),
55
104
  path.join(workspaceRoot, '.windsurf', 'hooks.json'),
56
- // ── Cline ────────────────────────────────────────────────────────
57
- path.join(workspaceRoot, '.clinerules', 'hooks', 'hooks.json'),
58
105
  // ── OpenHands ────────────────────────────────────────────────────
59
106
  path.join(workspaceRoot, '.openhands', 'hooks.json'),
60
107
  // ── Goose (immutable — incident on tamper, not silent restore) ───
61
- path.join(home, '.agents', 'plugins', 'intutic-governance', 'hooks', 'hooks.json'),
62
- // ── Roo Code ─────────────────────────────────────────────────────
63
- path.join(workspaceRoot, '.roorules'),
64
- // ── VS Code settings (Cline / Roo Code) ──────────────────────────
65
- path.join(home, '.config', 'Code', 'User', 'settings.json'),
66
- path.join(home, 'Library', 'Application Support', 'Code', 'User', 'settings.json'),
67
- path.join(workspaceRoot, '.vscode', 'settings.json'),
108
+ // The script is a gate artifact, listed above. The config names it
109
+ // too, as `hooks.pre_tool_use`.
110
+ path.join(GOOSE_PLUGIN_DIR, 'plugin.json'),
111
+ path.join(GOOSE_PLUGIN_DIR, 'hooks', 'hooks.json'),
112
+ path.join(home, '.config', 'goose', 'config.yaml'),
113
+ // ── Codex: the gate's registration at both levels ───────────────
114
+ path.join(workspaceRoot, '.codex', 'hooks.json'),
115
+ path.join(codexUserDir(), 'hooks.json'),
116
+ // ── GitHub Copilot: the hook file at both levels ────────────────
117
+ path.join(workspaceRoot, '.github', 'hooks', 'intutic-governance.json'),
118
+ path.join(home, '.copilot', 'hooks', 'intutic-governance.json'),
119
+ // ── Hermes: `hooks.pre_tool_call` registers the gate ────────────
120
+ path.join(home, '.hermes', 'config.yaml'),
121
+ // ── VS Code settings (Cline, Roo Code, the GitHub Copilot hooks) ──
122
+ ...vscodeSettingsFiles(workspaceRoot),
68
123
  // ── Claude Desktop ────────────────────────────────────────────────
69
124
  path.join(home, 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json'),
70
125
  path.join(home, '.config', 'Claude', 'claude_desktop_config.json'),
71
- // ── Antigravity ───────────────────────────────────────────────────
72
- path.join(workspaceRoot, '.gemini', 'settings.json'),
126
+ // ── Antigravity and Gemini CLI ────────────────────────────────────
127
+ // The gates are registered at user level: Gemini CLI's in
128
+ // ~/.gemini/settings.json, Antigravity's in ~/.gemini/config/hooks.json.
129
+ // The rules are in the workspace's GEMINI.md, which the drift watcher
130
+ // watches as the harness's rules file.
131
+ path.join(home, '.gemini', 'settings.json'),
132
+ path.join(home, '.gemini', 'config', 'hooks.json'),
73
133
  // ── Muse Code ────────────────────────────────────────────────────
74
134
  path.join(home, '.config', 'muse', 'settings.json'),
75
135
  path.join(home, '.config', 'muse', 'intutic-managed-hooks.json'),
76
136
  path.join(workspaceRoot, '.muse', 'hooks.json'),
77
- // ── OpenCode ─────────────────────────────────────────────────────
78
- path.join(workspaceRoot, OPENCODE_PLUGIN_DIR, OPENCODE_PLUGIN_FILE),
79
- path.join(workspaceRoot, OPENCODE_PLUGIN_DIR, OPENCODE_PLUGIN_V2_FILE),
137
+ // ── OpenClaw: the config that lists the plugin ──────────────────
138
+ path.join(home, '.openclaw', 'openclaw.json'),
80
139
  // ── Grok Build ───────────────────────────────────────────────────
81
140
  path.join(home, '.grok', 'hooks', 'intutic-governance.json'),
82
141
  path.join(workspaceRoot, '.grok', 'hooks', 'intutic-governance.json'),
@@ -103,11 +162,20 @@ export function buildProtectedPaths(workspaceRoot) {
103
162
  * once dsh creates it; `driftWatcher.ts` needs to know to react to `addDir`
104
163
  * for this ONE path — every other protected path only reacts to
105
164
  * `change`/`unlink` (see that module's own comment) — so this predicate is
106
- * exported for it to check against. TD-370.
165
+ * exported for it to check against.
107
166
  */
108
167
  export function isDshProfilesRoot(changedPath) {
109
168
  return changedPath === path.join(resolveDshHome(), 'profiles');
110
169
  }
170
+ /**
171
+ * Whether a change to `changedPath` is one {@link guardSettingsFile} judges:
172
+ * a path {@link buildProtectedPaths} lists, or a file under one it lists as a
173
+ * directory (dsh's profiles). The watcher's callback routes on this, so a
174
+ * path added to that list reaches the guard without a second edit there.
175
+ */
176
+ export function isGuardedPath(changedPath, workspaceRoot) {
177
+ return buildProtectedPaths(workspaceRoot).some((p) => changedPath === p || changedPath.startsWith(p + path.sep));
178
+ }
111
179
  /**
112
180
  * Resolves the workspace id for an incident or a restore.
113
181
  *
@@ -137,6 +205,61 @@ async function resolveWorkspaceId(workspaceRoot) {
137
205
  }
138
206
  return 'unknown';
139
207
  }
208
+ /**
209
+ * Appends a `config_tamper` event to the workspace's hook-events log, the file
210
+ * the daemon drains to `POST /api/v1/hook-events`: the control plane files it
211
+ * as an incident (on the audit timeline) and exports it to SIEM as a
212
+ * `TAMPER` gate decision. Never throws.
213
+ */
214
+ async function reportTamper(workspaceRoot, event) {
215
+ try {
216
+ const line = JSON.stringify({
217
+ event: 'config_tamper',
218
+ ...event,
219
+ workspaceId: await resolveWorkspaceId(workspaceRoot),
220
+ timestamp: new Date().toISOString(),
221
+ incidentId: crypto.createHash('sha1').update(event.filePath + Date.now()).digest('hex').slice(0, 16),
222
+ // Made once and resent with this line: processed once however often the drain retries.
223
+ eventId: crypto.randomBytes(16).toString('hex'),
224
+ }) + '\n';
225
+ const file = path.join(workspaceRoot, '.intutic', 'events', 'hook-events.jsonl');
226
+ await fs.mkdir(path.dirname(file), { recursive: true });
227
+ await fs.appendFile(file, line);
228
+ }
229
+ catch (err) {
230
+ log.warn({ err, filePath: event.filePath }, 'Failed to write a tamper event to the hook-events log');
231
+ }
232
+ }
233
+ /**
234
+ * The policy snapshot's self-heal. When the live snapshot differs from the
235
+ * last one the daemon verified, the verified copy goes back in force at once;
236
+ * when there is no verified copy and the live one fails its digest, `resync`
237
+ * fetches a fresh one. Either way the tamper is reported. A gate reading the
238
+ * damaged snapshot in between refuses every MCP call
239
+ * (`POLICY_SNAPSHOT_UNVERIFIED`) and drops its other dynamic rules. Returns
240
+ * whether it found tampering.
241
+ */
242
+ export async function guardPolicySnapshot(workspaceRoot, resync, snapshotDir = DEFAULT_SNAPSHOT_DIR) {
243
+ const outcome = await restoreVerifiedSnapshot(snapshotDir);
244
+ if (outcome === 'intact')
245
+ return false;
246
+ if (outcome === 'unverifiable') {
247
+ try {
248
+ await resync();
249
+ }
250
+ catch (err) {
251
+ log.error({ action: 'policy_snapshot_resync_failed', err }, 'Could not fetch a fresh policy snapshot');
252
+ }
253
+ }
254
+ await reportTamper(workspaceRoot, {
255
+ toolName: 'policy_snapshot',
256
+ filePath: path.join(snapshotDir, SNAPSHOT_RULES),
257
+ reason: outcome === 'restored'
258
+ ? 'The policy snapshot was changed outside the sync daemon; the last verified snapshot was restored.'
259
+ : 'The policy snapshot failed its digest check and no verified copy was kept to restore; a fresh one was requested.',
260
+ });
261
+ return true;
262
+ }
140
263
  // ─── Public entry point ──────────────────────────────────────────────
141
264
  /**
142
265
  * Inspect any protected governance config file and restore if tampered.
@@ -146,31 +269,43 @@ async function resolveWorkspaceId(workspaceRoot) {
146
269
  * @param sops - Current SOP list.
147
270
  * @param proxyUrl - Current proxy URL.
148
271
  * @param settings - Optional passthrough settings.
272
+ * @param skip - Harnesses `intutic disconnect --harness` took out:
273
+ * their files are no longer governed, so a change to
274
+ * one is not tampering and is not restored.
275
+ * @param resyncPolicy - Fetches a fresh policy snapshot, for a damaged one
276
+ * with no verified copy to restore.
149
277
  * @returns true if tampering was detected.
150
278
  */
151
- export async function guardSettingsFile(changedPath, workspaceRoot, sops, proxyUrl = '', settings) {
279
+ export async function guardSettingsFile(changedPath, workspaceRoot, sops, proxyUrl = '', settings, skip = new Set(), resyncPolicy = async () => { }) {
280
+ // ── The policy snapshot: the last verified copy, back in force ────
281
+ if (policySnapshotFiles().includes(changedPath)) {
282
+ return guardPolicySnapshot(workspaceRoot, resyncPolicy);
283
+ }
284
+ // ── VS Code settings: the keys that switch off the Copilot gate ───
285
+ // Only while the gate is installed, and only those keys; anything else in
286
+ // the file is the person's own. A deletion falls through to drift below.
287
+ if (vscodeSettingsFiles(workspaceRoot).includes(changedPath) && !skip.has('github-copilot')) {
288
+ const repaired = await guardVsCodeHookSettings(changedPath, workspaceRoot);
289
+ if (repaired !== null)
290
+ return repaired;
291
+ }
152
292
  // ── Goose plugin: immutable file tamper → incident, not restore ───
153
- if (changedPath.includes('intutic-governance')) {
293
+ // The plugin directory, not the bare name: the OpenCode, Pi and OpenClaw
294
+ // gate files are called intutic-governance too, and a name match sent
295
+ // their tampering here, to the Goose writer.
296
+ if (changedPath.includes(path.join('.agents', 'plugins', 'intutic-governance'))) {
297
+ if (skip.has('goose'))
298
+ return false;
154
299
  if (await isImmutable(changedPath)) {
155
300
  log.error({ action: 'governance_override_attempt', path: changedPath }, 'SECURITY: Immutable Goose governance file was modified — OS immutable flag bypassed. Emitting incident.');
156
- const workspaceId = await resolveWorkspaceId(workspaceRoot);
157
- // Emit incident to control plane via local hook-events queue
158
- try {
159
- const tamperEntry = JSON.stringify({
160
- event: 'config_tamper',
161
- toolName: 'goose_plugin',
162
- reason: 'SECURITY: Immutable Goose governance file was modified — OS immutable flag bypassed.',
163
- workspaceId,
164
- filePath: changedPath,
165
- timestamp: new Date().toISOString(),
166
- incidentId: crypto.createHash('sha1').update(changedPath + Date.now()).digest('hex').slice(0, 16),
167
- }) + '\n';
168
- const hookEventsJsonl = path.join(os.homedir(), '.intutic', 'events', 'hook-events.jsonl');
169
- await fs.appendFile(hookEventsJsonl, tamperEntry, { flag: 'a' });
170
- }
171
- catch (err) {
172
- log.warn({ err }, 'Failed to write Goose tamper incident to hook-events log');
173
- }
301
+ // The workspace's log, which the daemon drains; one under the home
302
+ // directory was never read unless the workspace was the home directory.
303
+ await reportTamper(workspaceRoot, {
304
+ toolName: 'goose_plugin',
305
+ harnessType: 'goose',
306
+ reason: 'SECURITY: Immutable Goose governance file was modified — OS immutable flag bypassed.',
307
+ filePath: changedPath,
308
+ });
174
309
  return true;
175
310
  }
176
311
  // Not immutable (install in progress or first write) — restore normally.
@@ -185,30 +320,62 @@ export async function guardSettingsFile(changedPath, workspaceRoot, sops, proxyU
185
320
  await writeGooseHooks(proxyUrl, workspaceRoot, await resolveWorkspaceId(workspaceRoot));
186
321
  return true;
187
322
  }
323
+ // ── A gate file: restored by the writer that generates it ─────────
324
+ const gate = gateArtifactPaths(workspaceRoot).find((g) => g.file === changedPath);
325
+ if (gate) {
326
+ if (skip.has(gate.harness))
327
+ return false;
328
+ // Cline's writer leaves a PreToolUse it did not write alone, as the
329
+ // user's own hook; the ledger says whether it wrote this one.
330
+ if (gate.harness === 'cline' && !(await readOriginal(changedPath, workspaceRoot))?.writtenSha256)
331
+ return false;
332
+ return guardGateFile(changedPath, gate.harness, async () => GATE_WRITERS[gate.harness]({ workspaceRoot, proxyUrl, sops, settings, workspaceId: await resolveWorkspaceId(workspaceRoot), file: changedPath }));
333
+ }
188
334
  // ── Claude Code settings.json ─────────────────────────────────────
189
335
  if (changedPath.includes('.claude') && changedPath.endsWith('settings.json')) {
336
+ if (skip.has('claude-code'))
337
+ return false;
190
338
  return guardClaudeCodeSettings(changedPath, workspaceRoot, sops, settings);
191
339
  }
340
+ // ── Gemini CLI and Antigravity: the user-level gate registrations ──
341
+ // Restored whatever the hand-edit setting says, like every gate file:
342
+ // they are the gates, not the user's config.
343
+ if (changedPath === path.join(os.homedir(), '.gemini', 'settings.json')) {
344
+ if (skip.has('antigravity'))
345
+ return false;
346
+ return guardParsedFile(changedPath, 'antigravity', hasGeminiGate, async () => {
347
+ await writeAntigravityHooks(workspaceRoot, proxyUrl, await resolveWorkspaceId(workspaceRoot));
348
+ });
349
+ }
350
+ if (changedPath === antigravityHooksPath()) {
351
+ if (skip.has('antigravity'))
352
+ return false;
353
+ return guardParsedFile(changedPath, 'antigravity', hasAntigravityGate, async () => {
354
+ await writeAntigravityCliHooks(workspaceRoot, proxyUrl, await resolveWorkspaceId(workspaceRoot));
355
+ });
356
+ }
192
357
  // ── Cursor hooks.json ─────────────────────────────────────────────
193
- if (changedPath.includes('.cursor') && changedPath.endsWith('hooks.json')) {
358
+ // The machine-wide file is in no `.cursor` directory (`systemHooksDirFor`).
359
+ const systemLevel = changedPath === cursorSystemHooks();
360
+ if ((changedPath.includes('.cursor') || systemLevel) && changedPath.endsWith('hooks.json')) {
361
+ if (skip.has('cursor'))
362
+ return false;
194
363
  return guardJsonHookFile(changedPath, 'cursor', async () => {
195
- await writeCursorHooks(workspaceRoot, proxyUrl, '', changedPath.startsWith('/etc'));
364
+ await writeCursorHooks(workspaceRoot, proxyUrl, '', systemLevel);
196
365
  });
197
366
  }
198
367
  // ── Windsurf hooks.json ───────────────────────────────────────────
199
368
  if ((changedPath.includes('.codeium') || changedPath.includes('.windsurf')) && changedPath.endsWith('hooks.json')) {
369
+ if (skip.has('windsurf'))
370
+ return false;
200
371
  return guardJsonHookFile(changedPath, 'windsurf', async () => {
201
372
  await writeWindsurfHooks(workspaceRoot, proxyUrl);
202
373
  });
203
374
  }
204
- // ── Cline hooks.json ──────────────────────────────────────────────
205
- if (changedPath.includes('.clinerules') && changedPath.endsWith('hooks.json')) {
206
- return guardJsonHookFile(changedPath, 'cline', async () => {
207
- await writeClineHooks(workspaceRoot, proxyUrl);
208
- });
209
- }
210
375
  // ── OpenHands hooks.json ──────────────────────────────────────────
211
376
  if (changedPath.includes('.openhands') && changedPath.endsWith('hooks.json')) {
377
+ if (skip.has('openhands'))
378
+ return false;
212
379
  return guardJsonHookFile(changedPath, 'openhands', async () => {
213
380
  await writeOpenHandsHooks(workspaceRoot, proxyUrl);
214
381
  });
@@ -219,6 +386,8 @@ export async function guardSettingsFile(changedPath, workspaceRoot, sops, proxyU
219
386
  if ((changedPath.includes(path.join('.muse', 'hooks.json'))) ||
220
387
  (changedPath.includes(path.join('.config', 'muse')) &&
221
388
  (changedPath.endsWith('settings.json') || changedPath.endsWith('intutic-managed-hooks.json')))) {
389
+ if (skip.has('muse-code'))
390
+ return false;
222
391
  return guardJsonHookFile(changedPath, 'muse-code', async () => {
223
392
  await writeMuseHooks(workspaceRoot, proxyUrl, await resolveWorkspaceId(workspaceRoot));
224
393
  });
@@ -230,6 +399,8 @@ export async function guardSettingsFile(changedPath, workspaceRoot, sops, proxyU
230
399
  // a tamper there falls through to the generic drift-log path below rather
231
400
  // than a targeted restore.
232
401
  if (changedPath.includes('.grok') && changedPath.endsWith('intutic-governance.json')) {
402
+ if (skip.has('grok'))
403
+ return false;
233
404
  return guardJsonHookFile(changedPath, 'grok', async () => {
234
405
  await writeGrokHooks(workspaceRoot, proxyUrl);
235
406
  });
@@ -238,10 +409,12 @@ export async function guardSettingsFile(changedPath, workspaceRoot, sops, proxyU
238
409
  // --profile <name>` run on this machine) ──────────────────────────────
239
410
  // driftWatcher.ts forwards this here on chokidar's `addDir` event (see
240
411
  // isDshProfilesRoot) — register governance into whichever profile(s) now
241
- // exist immediately, closing TD-370's silent window at the moment it
412
+ // exist immediately, closing dsh's silent no-profile window the moment it
242
413
  // closes itself, rather than waiting for an unrelated file change or the
243
414
  // next poll cycle to notice.
244
415
  if (isDshProfilesRoot(changedPath)) {
416
+ if (skip.has('dsh'))
417
+ return false;
245
418
  await safeRestore('dsh', () => writeDshHooks(workspaceRoot, proxyUrl, ''));
246
419
  return true;
247
420
  }
@@ -258,13 +431,19 @@ export async function guardSettingsFile(changedPath, workspaceRoot, sops, proxyU
258
431
  // longer writes it.
259
432
  if (changedPath.startsWith(path.join(resolveDshHome(), 'profiles') + path.sep) &&
260
433
  changedPath.endsWith('cordis.patch.yml')) {
434
+ if (skip.has('dsh'))
435
+ return false;
261
436
  return guardDshFile(changedPath, ['intutic-governance', proxyUrl], workspaceRoot, proxyUrl);
262
437
  }
263
- // ── OpenCode plugin: marker-substring guard, same shape as dsh ────
264
- // The gate body stamps itself; a file without the stamp is not ours.
265
- if (changedPath === path.join(workspaceRoot, OPENCODE_PLUGIN_DIR, OPENCODE_PLUGIN_FILE) ||
266
- changedPath === path.join(workspaceRoot, OPENCODE_PLUGIN_DIR, OPENCODE_PLUGIN_V2_FILE)) {
267
- return guardMarkedFile(changedPath, 'Intutic gate body', 'opencode', () => writeOpenCodeHooks(workspaceRoot, proxyUrl, ''));
438
+ // ── Configs that name the gate ────────────────────────────────────
439
+ // Each is the user's file as much as ours, and exists whether or not the
440
+ // harness is connected: only while the gate is installed is a config that
441
+ // no longer names it tampered with. Its marker is the gate's file name.
442
+ const registration = gateRegistrations(workspaceRoot).find((r) => r.file === changedPath);
443
+ if (registration) {
444
+ if (skip.has(registration.harness) || !(await fileExists(registration.gate)))
445
+ return false;
446
+ return guardMarkedFile(changedPath, registration.marker, registration.harness, async () => GATE_WRITERS[registration.harness]({ workspaceRoot, proxyUrl, sops, settings, workspaceId: await resolveWorkspaceId(workspaceRoot), file: changedPath }));
268
447
  }
269
448
  // ── All other paths: file deleted or corrupted → log drift incident
270
449
  const exists = await fileExists(changedPath);
@@ -277,6 +456,122 @@ export async function guardSettingsFile(changedPath, workspaceRoot, sops, proxyU
277
456
  return false;
278
457
  }
279
458
  // ─── Internal helpers ────────────────────────────────────────────────
459
+ /**
460
+ * Sets `chat.useHooks`, and any `chat.hookFilesLocations` entry that drops the
461
+ * Copilot gate's location, back to `true` (vscodeHookSettings.ts). Null when
462
+ * this is not the guard's to judge: the Copilot gate is not installed, or the
463
+ * file is gone or not a settings object.
464
+ */
465
+ async function guardVsCodeHookSettings(filePath, workspaceRoot) {
466
+ const copilotGate = path.join(workspaceRoot, '.intutic', 'hooks', 'github-copilot-check.js');
467
+ if (!(await fileExists(copilotGate)))
468
+ return null;
469
+ let raw;
470
+ try {
471
+ raw = await fs.readFile(filePath, 'utf-8');
472
+ }
473
+ catch {
474
+ return null;
475
+ }
476
+ const repaired = repairHookSettings(raw, workspaceRoot);
477
+ if (!repaired)
478
+ return false;
479
+ try {
480
+ await fs.writeFile(filePath, repaired.text, 'utf-8');
481
+ }
482
+ catch (err) {
483
+ log.error({ action: 'vscode_hook_settings_restore_failed', path: filePath, err }, 'Could not restore the VS Code hook settings');
484
+ }
485
+ log.warn({ action: 'vscode_hook_settings_restored', path: filePath, keys: repaired.keys }, 'A VS Code setting switched off the GitHub Copilot gate — set back');
486
+ await reportTamper(workspaceRoot, {
487
+ toolName: 'vscode_settings',
488
+ harnessType: 'github-copilot',
489
+ filePath,
490
+ reason: `VS Code setting ${repaired.keys.join(', ')} switched off the GitHub Copilot governance hook; set back to true.`,
491
+ });
492
+ return true;
493
+ }
494
+ /** The writer that regenerates each harness's gate and its registrations. */
495
+ const GATE_WRITERS = {
496
+ 'claude-code': (c) => updatePreToolUseHooks(c.workspaceRoot, c.sops, c.settings),
497
+ 'cursor': (c) => writeCursorHooks(c.workspaceRoot, c.proxyUrl, c.workspaceId),
498
+ 'windsurf': (c) => writeWindsurfHooks(c.workspaceRoot, c.proxyUrl, undefined, c.workspaceId),
499
+ 'codex': (c) => writeCodexHooks(c.workspaceRoot, c.proxyUrl, c.workspaceId),
500
+ 'antigravity': async (c) => {
501
+ await writeAntigravityHooks(c.workspaceRoot, c.proxyUrl, c.workspaceId);
502
+ await writeAntigravityCliHooks(c.workspaceRoot, c.proxyUrl, c.workspaceId);
503
+ },
504
+ 'openhands': (c) => writeOpenHandsHooks(c.workspaceRoot, c.proxyUrl, c.workspaceId),
505
+ 'hermes': (c) => writeHermesHooks(c.workspaceRoot, c.proxyUrl, c.workspaceId),
506
+ 'pi': (c) => writePiHooks(c.workspaceRoot, c.proxyUrl, c.workspaceId),
507
+ 'openclaw': (c) => writeOpenclawHooks(c.workspaceRoot, c.proxyUrl, c.workspaceId),
508
+ 'muse-code': (c) => writeMuseHooks(c.workspaceRoot, c.proxyUrl, c.workspaceId),
509
+ 'github-copilot': (c) => writeGithubCopilotHooks(c.workspaceRoot, c.proxyUrl, c.workspaceId),
510
+ 'grok': (c) => writeGrokHooks(c.workspaceRoot, c.proxyUrl, c.workspaceId),
511
+ 'cline': async (c) => {
512
+ // Only a PreToolUse Intutic wrote is restored (see the gate branch), so
513
+ // whatever is there now replaced the gate. The writer would keep it as
514
+ // the user's own hook.
515
+ await fs.rm(c.file, { force: true });
516
+ await writeClineHooks(c.workspaceRoot, c.proxyUrl, c.workspaceId);
517
+ },
518
+ 'goose': (c) => writeGooseHooks(c.proxyUrl, c.workspaceRoot, c.workspaceId),
519
+ 'opencode': (c) => writeOpenCodeHooks(c.workspaceRoot, c.proxyUrl, c.workspaceId),
520
+ 'n8n': (c) => writeN8nHooks(c.workspaceRoot, c.proxyUrl, c.workspaceId),
521
+ 'open-webui': (c) => writeOpenWebuiHooks(c.workspaceRoot, c.proxyUrl, c.workspaceId),
522
+ };
523
+ function gateRegistrations(workspaceRoot) {
524
+ const codexGate = path.join(workspaceRoot, '.intutic', 'hooks', 'codex-check.js');
525
+ const copilotGate = path.join(workspaceRoot, '.intutic', 'hooks', 'github-copilot-check.js');
526
+ const openclawPlugin = path.join(home, OPENCLAW_PLUGIN_FILE);
527
+ return [
528
+ { harness: 'codex', file: path.join(workspaceRoot, '.codex', 'hooks.json'), gate: codexGate, marker: 'codex-check.js' },
529
+ { harness: 'codex', file: path.join(codexUserDir(), 'hooks.json'), gate: codexGate, marker: 'codex-check.js' },
530
+ {
531
+ harness: 'github-copilot',
532
+ file: path.join(workspaceRoot, '.github', 'hooks', 'intutic-governance.json'),
533
+ gate: copilotGate,
534
+ marker: 'github-copilot-check.js',
535
+ },
536
+ {
537
+ harness: 'github-copilot',
538
+ file: path.join(home, '.copilot', 'hooks', 'intutic-governance.json'),
539
+ gate: copilotGate,
540
+ marker: 'github-copilot-check.js',
541
+ },
542
+ {
543
+ harness: 'hermes',
544
+ file: path.join(home, '.hermes', 'config.yaml'),
545
+ gate: path.join(home, '.intutic', 'hooks', 'hermes-check.sh'),
546
+ marker: 'hermes-check.sh',
547
+ },
548
+ { harness: 'goose', file: path.join(home, '.config', 'goose', 'config.yaml'), gate: GOOSE_GATE_SCRIPT, marker: GOOSE_GATE_SCRIPT },
549
+ // OpenClaw rewrites its own config; its marker is the listed plugin path.
550
+ { harness: 'openclaw', file: path.join(home, '.openclaw', 'openclaw.json'), gate: openclawPlugin, marker: openclawPlugin },
551
+ ];
552
+ }
553
+ /**
554
+ * A gate file: deleted, or no longer the generated gate, and its writer runs
555
+ * again. Cline runs its hook only while the file is executable, so for Cline
556
+ * a cleared execute bit is tampering too.
557
+ */
558
+ async function guardGateFile(filePath, harness, restore) {
559
+ let raw;
560
+ try {
561
+ raw = await fs.readFile(filePath, 'utf-8');
562
+ }
563
+ catch {
564
+ log.warn({ action: 'gate_file_deleted', harness, path: filePath }, `${harness} gate deleted — restoring`);
565
+ await safeRestore(harness, restore);
566
+ return true;
567
+ }
568
+ const disabled = harness === 'cline' && ((await fs.stat(filePath)).mode & 0o111) === 0;
569
+ if (raw.includes(GATE_BODY_MARKER) && !disabled)
570
+ return false;
571
+ log.warn({ action: 'gate_file_tampered', harness, path: filePath }, `${harness} gate tampered — restoring`);
572
+ await safeRestore(harness, restore);
573
+ return true;
574
+ }
280
575
  async function guardClaudeCodeSettings(settingsFilePath, workspaceRoot, sops, settings) {
281
576
  let raw;
282
577
  try {
@@ -395,7 +690,7 @@ async function guardDshFile(filePath, markers, workspaceRoot, proxyUrl) {
395
690
  }
396
691
  /**
397
692
  * Side-effecting wrapper around `dshHooks.ts`'s pure `detectDshCoverageGap` —
398
- * logs TD-370's "silent no-profile window" as a `dsh_coverage_gap` warning
693
+ * logs dsh's "silent no-profile window" as a `dsh_coverage_gap` warning
399
694
  * when dsh is present on this machine but has zero profiles, so the window
400
695
  * is at least documented in the logs even though there is nothing to
401
696
  * restore into yet. Called once at `intutic connect` startup — see
@@ -408,11 +703,53 @@ export async function warnIfDshCoverageGap() {
408
703
  const result = await detectDshCoverageGap();
409
704
  if (result.gap) {
410
705
  log.warn({ action: 'dsh_coverage_gap', dshDetected: result.dshDetected, profileCount: result.profileCount }, 'dsh is present on this machine but has zero profiles yet — nothing is governed until the ' +
411
- 'first `dsh --profile <name>` run creates one (TD-370). The next sync after that run picks ' +
706
+ 'first `dsh --profile <name>` run creates one. The next sync after that run picks ' +
412
707
  'it up automatically; this warning is visibility for the window before it, not a fix for it.');
413
708
  }
414
709
  return result.gap;
415
710
  }
711
+ /**
712
+ * A file whose integrity is a fact about its parsed content: deleted,
713
+ * unparseable, or parsed without the gate → restored. A marker-substring
714
+ * check is not enough here: these are the user's own settings files, and
715
+ * the MCP writer adds an `intutic` server to the same settings.json. The
716
+ * writers leave a file that is not a plain JSON object alone, so for one of
717
+ * those the restore is a logged no-op and the drift report surfaces it.
718
+ */
719
+ async function guardParsedFile(filePath, harness, intact, restore) {
720
+ let doc;
721
+ try {
722
+ doc = JSON.parse(await fs.readFile(filePath, 'utf-8'));
723
+ }
724
+ catch {
725
+ log.warn({ action: `${harness}_file_unreadable`, path: filePath }, `${harness} governance file deleted or not JSON — restoring`);
726
+ await safeRestore(harness, restore);
727
+ return true;
728
+ }
729
+ if (intact(doc))
730
+ return false;
731
+ log.warn({ action: `${harness}_hook_missing`, path: filePath }, `${harness} governance hook missing — restoring`);
732
+ await safeRestore(harness, restore);
733
+ return true;
734
+ }
735
+ function isObj(v) {
736
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
737
+ }
738
+ /** Whether a `{matcher, hooks: [{command}]}` list has an entry running `script`. */
739
+ function runsScript(entries, script) {
740
+ return Array.isArray(entries) && entries.some((e) => isObj(e) && Array.isArray(e.hooks) && e.hooks.some((h) => isObj(h) && typeof h.command === 'string' && h.command.includes(script)));
741
+ }
742
+ /** Gemini CLI's settings.json still registers the `BeforeTool` gate. */
743
+ function hasGeminiGate(doc) {
744
+ return isObj(doc) && isObj(doc.hooks) && runsScript(doc.hooks.BeforeTool, 'antigravity-check.sh');
745
+ }
746
+ /** Antigravity's hooks.json still carries Intutic's entry, enabled, running the gate. */
747
+ function hasAntigravityGate(doc) {
748
+ if (!isObj(doc))
749
+ return false;
750
+ const entry = doc[ANTIGRAVITY_HOOK_NAME];
751
+ return isObj(entry) && entry.enabled !== false && runsScript(entry.PreToolUse, ANTIGRAVITY_CLI_GATE);
752
+ }
416
753
  async function safeRestore(harness, restore) {
417
754
  try {
418
755
  await restore();