multicorn-shield 1.2.0 → 1.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.
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env node
2
- import { existsSync, statSync } from 'fs';
2
+ import { readFileSync, existsSync, statSync } from 'fs';
3
3
  import { readFile, mkdir, writeFile, copyFile, chmod, unlink } from 'fs/promises';
4
4
  import { join, dirname, resolve, basename, sep } from 'path';
5
5
  import { homedir } from 'os';
6
6
  import { fileURLToPath } from 'url';
7
+ import { createRequire } from 'module';
7
8
  import { createInterface } from 'readline';
8
9
  import { spawn } from 'child_process';
9
10
  import { createHash } from 'crypto';
@@ -391,6 +392,32 @@ function isExistingDirectory(path) {
391
392
  return false;
392
393
  }
393
394
  }
395
+ function jsonValueMentionsMulticornShield(value) {
396
+ if (typeof value === "string") {
397
+ return value.includes("multicorn-shield");
398
+ }
399
+ if (Array.isArray(value)) {
400
+ return value.some(jsonValueMentionsMulticornShield);
401
+ }
402
+ if (value !== null && typeof value === "object") {
403
+ for (const [k, v] of Object.entries(value)) {
404
+ if (k.includes("multicorn-shield")) return true;
405
+ if (jsonValueMentionsMulticornShield(v)) return true;
406
+ }
407
+ }
408
+ return false;
409
+ }
410
+ function claudeInstalledPluginsListsMulticornShield() {
411
+ const path = join(homedir(), ".claude", "plugins", "installed_plugins.json");
412
+ if (!existsSync(path)) return false;
413
+ try {
414
+ const raw = readFileSync(path, "utf8");
415
+ const parsed = JSON.parse(raw);
416
+ return jsonValueMentionsMulticornShield(parsed);
417
+ } catch {
418
+ return false;
419
+ }
420
+ }
394
421
  function nativePluginSkippedSaveNote(wizardCommand, productName) {
395
422
  return "\n" + style.dim("Your agent config has been saved. Run ") + style.cyan(wizardCommand) + style.dim(` again after installing ${productName} to complete hook setup.`) + "\n";
396
423
  }
@@ -705,78 +732,48 @@ async function validateApiKey(apiKey, baseUrl) {
705
732
  };
706
733
  }
707
734
  }
708
- async function isOpenClawConnected() {
709
- try {
710
- const raw = await readFile(OPENCLAW_CONFIG_PATH, "utf8");
711
- const obj = JSON.parse(raw);
712
- const hooks = obj["hooks"];
713
- const internal = hooks?.["internal"];
714
- const entries = internal?.["entries"];
715
- const shield = entries?.["multicorn-shield"];
716
- const env = shield?.["env"];
717
- const key = env?.["MULTICORN_API_KEY"];
718
- return typeof key === "string" && key.length > 0;
719
- } catch {
720
- return false;
721
- }
735
+ function multicornShieldPackageRoot() {
736
+ return join(dirname(fileURLToPath(import.meta.url)), "..");
722
737
  }
723
- function isClaudeCodeConnected() {
738
+ function multicornShieldInstallRoot() {
724
739
  try {
725
- return existsSync(join(homedir(), ".claude", "plugins", "cache", "multicorn-shield"));
740
+ const req = createRequire(import.meta.url);
741
+ return dirname(req.resolve("multicorn-shield/package.json"));
726
742
  } catch {
727
- return false;
743
+ return multicornShieldPackageRoot();
728
744
  }
729
745
  }
730
- function getCursorConfigPath() {
731
- return join(homedir(), ".cursor", "mcp.json");
746
+ function shieldInstalledVersionOlderThan(latest, installed) {
747
+ return latest.localeCompare(installed, void 0, { numeric: true }) > 0;
732
748
  }
733
- async function isCursorConnected() {
749
+ async function warnIfInstalledShieldIsOutdated() {
734
750
  try {
735
- const raw = await readFile(getCursorConfigPath(), "utf8");
736
- const obj = JSON.parse(raw);
737
- const mcpServers = obj["mcpServers"];
738
- if (mcpServers === void 0 || typeof mcpServers !== "object") return false;
739
- for (const entry of Object.values(mcpServers)) {
740
- if (typeof entry !== "object" || entry === null) continue;
741
- const rec = entry;
742
- const url = rec["url"];
743
- if (typeof url === "string" && url.includes("multicorn")) return true;
744
- const args = rec["args"];
745
- if (Array.isArray(args) && (args.includes("multicorn-shield") || args.includes("multicorn-proxy")))
746
- return true;
751
+ const res = await fetch("https://registry.npmjs.org/multicorn-shield/latest");
752
+ if (!res.ok) return;
753
+ const data = await res.json();
754
+ const latest = typeof data.version === "string" ? data.version : "";
755
+ if (latest.length === 0) return;
756
+ let installed = "";
757
+ try {
758
+ const req = createRequire(import.meta.url);
759
+ const pkgPath = req.resolve("multicorn-shield/package.json");
760
+ const raw = readFileSync(pkgPath, "utf8");
761
+ const pkg = JSON.parse(raw);
762
+ installed = typeof pkg.version === "string" ? pkg.version : "";
763
+ } catch {
764
+ return;
765
+ }
766
+ if (installed.length === 0 || !shieldInstalledVersionOlderThan(latest, installed)) {
767
+ return;
747
768
  }
748
- return false;
749
- } catch (err) {
750
769
  process.stderr.write(
751
- `Warning: could not check Cursor connection status: ${err instanceof Error ? err.message : String(err)}
770
+ style.yellow("\u26A0") + ` multicorn-shield v${installed} is installed but v${latest} is available. Run npm update multicorn-shield to update.
771
+
752
772
  `
753
773
  );
754
- return false;
755
- }
756
- }
757
- function getWindsurfConfigPath() {
758
- return join(homedir(), ".codeium", "windsurf", "mcp_config.json");
759
- }
760
- async function isWindsurfConnected() {
761
- try {
762
- const raw = await readFile(getWindsurfConfigPath(), "utf8");
763
- const obj = JSON.parse(raw);
764
- const mcpServers = obj["mcpServers"];
765
- if (mcpServers === void 0 || typeof mcpServers !== "object") return false;
766
- for (const entry of Object.values(mcpServers)) {
767
- if (typeof entry !== "object" || entry === null) continue;
768
- const rec = entry;
769
- const url = rec["serverUrl"];
770
- if (typeof url === "string" && url.includes("multicorn")) return true;
771
- }
772
- return false;
773
774
  } catch {
774
- return false;
775
775
  }
776
776
  }
777
- function multicornShieldPackageRoot() {
778
- return join(dirname(fileURLToPath(import.meta.url)), "..");
779
- }
780
777
  function getWindsurfHooksInstallDir() {
781
778
  return join(homedir(), ".multicorn", "windsurf-hooks");
782
779
  }
@@ -1006,6 +1003,140 @@ function geminiStripMulticornHookEntries(hooks) {
1006
1003
  out["AfterTool"] = geminiStripMatcherGroups(out["AfterTool"]);
1007
1004
  return out;
1008
1005
  }
1006
+ function getClaudeCodeUserSettingsPath() {
1007
+ return join(homedir(), ".claude", "settings.json");
1008
+ }
1009
+ function commandLooksLikeMulticornClaudePre(cmd) {
1010
+ return typeof cmd === "string" && cmd.includes("pre-tool-use.cjs") && cmd.includes("multicorn-shield");
1011
+ }
1012
+ function commandLooksLikeMulticornClaudePost(cmd) {
1013
+ return typeof cmd === "string" && cmd.includes("post-tool-use.cjs") && cmd.includes("multicorn-shield");
1014
+ }
1015
+ function claudeSettingsMatcherGroupReferencesShield(group, kind) {
1016
+ const inner = group["hooks"];
1017
+ if (!Array.isArray(inner)) return false;
1018
+ const pred = kind === "pre" ? commandLooksLikeMulticornClaudePre : commandLooksLikeMulticornClaudePost;
1019
+ for (const h of inner) {
1020
+ if (typeof h !== "object" || h === null) continue;
1021
+ const rec = h;
1022
+ if (pred(rec["command"])) return true;
1023
+ }
1024
+ return false;
1025
+ }
1026
+ function claudeHooksHaveShieldEntries(hooks) {
1027
+ for (const key of ["PreToolUse", "PostToolUse"]) {
1028
+ const arr = hooks[key];
1029
+ if (!Array.isArray(arr)) continue;
1030
+ const kind = key === "PreToolUse" ? "pre" : "post";
1031
+ for (const g of arr) {
1032
+ if (typeof g === "object" && g !== null) {
1033
+ if (claudeSettingsMatcherGroupReferencesShield(g, kind)) {
1034
+ return true;
1035
+ }
1036
+ }
1037
+ }
1038
+ }
1039
+ return false;
1040
+ }
1041
+ function stripClaudeShieldHookGroups(arr, kind) {
1042
+ return arr.filter((g) => {
1043
+ if (typeof g !== "object" || g === null) return true;
1044
+ return !claudeSettingsMatcherGroupReferencesShield(g, kind);
1045
+ });
1046
+ }
1047
+ async function installClaudeCodeUserSettingsHooks(ask) {
1048
+ const root = multicornShieldInstallRoot();
1049
+ const prePath = join(root, "plugins", "multicorn-shield", "hooks", "scripts", "pre-tool-use.cjs");
1050
+ const postPath = join(
1051
+ root,
1052
+ "plugins",
1053
+ "multicorn-shield",
1054
+ "hooks",
1055
+ "scripts",
1056
+ "post-tool-use.cjs"
1057
+ );
1058
+ if (!existsSync(prePath) || !existsSync(postPath)) {
1059
+ process.stderr.write(
1060
+ style.red(
1061
+ "Could not find Shield Claude Code hook scripts next to the multicorn-shield package.\n"
1062
+ )
1063
+ );
1064
+ process.stderr.write(style.dim(` Expected: ${prePath}`) + "\n");
1065
+ return false;
1066
+ }
1067
+ const settingsPath = getClaudeCodeUserSettingsPath();
1068
+ await mkdir(dirname(settingsPath), { recursive: true });
1069
+ let existing = {};
1070
+ try {
1071
+ const rawText = await readFile(settingsPath, "utf8");
1072
+ const parsed = JSON.parse(rawText);
1073
+ if (parsed !== null && typeof parsed === "object" && !Array.isArray(parsed)) {
1074
+ existing = parsed;
1075
+ }
1076
+ } catch (err) {
1077
+ if (isErrnoException(err) && err.code === "ENOENT") {
1078
+ existing = {};
1079
+ } else {
1080
+ process.stderr.write(
1081
+ style.yellow("\u26A0") + ` Could not parse ${settingsPath}. Fix or remove the file, then run init again.
1082
+ `
1083
+ );
1084
+ return false;
1085
+ }
1086
+ }
1087
+ const hooksRaw = existing["hooks"];
1088
+ const hooksObj = typeof hooksRaw === "object" && hooksRaw !== null && !Array.isArray(hooksRaw) ? { ...hooksRaw } : {};
1089
+ if (claudeHooksHaveShieldEntries(hooksObj)) {
1090
+ const answer = await ask(
1091
+ "Existing Multicorn Shield hooks were found in ~/.claude/settings.json. Overwrite? (Y/n) "
1092
+ );
1093
+ if (answer.trim().toLowerCase() === "n") {
1094
+ return false;
1095
+ }
1096
+ }
1097
+ const preCmd = `node ${JSON.stringify(prePath)}`;
1098
+ const postCmd = `node ${JSON.stringify(postPath)}`;
1099
+ const preArr = stripClaudeShieldHookGroups(
1100
+ Array.isArray(hooksObj["PreToolUse"]) ? [...hooksObj["PreToolUse"]] : [],
1101
+ "pre"
1102
+ );
1103
+ const postArr = stripClaudeShieldHookGroups(
1104
+ Array.isArray(hooksObj["PostToolUse"]) ? [...hooksObj["PostToolUse"]] : [],
1105
+ "post"
1106
+ );
1107
+ preArr.push({
1108
+ matcher: "*",
1109
+ hooks: [
1110
+ {
1111
+ type: "command",
1112
+ name: "multicorn-shield-pre",
1113
+ command: preCmd,
1114
+ timeout: 600
1115
+ }
1116
+ ]
1117
+ });
1118
+ postArr.push({
1119
+ matcher: "*",
1120
+ hooks: [
1121
+ {
1122
+ type: "command",
1123
+ name: "multicorn-shield-post",
1124
+ command: postCmd,
1125
+ timeout: 120
1126
+ }
1127
+ ]
1128
+ });
1129
+ hooksObj["PreToolUse"] = preArr;
1130
+ hooksObj["PostToolUse"] = postArr;
1131
+ const out = { ...existing, hooks: hooksObj };
1132
+ const serialized = JSON.stringify(out, null, 2) + "\n";
1133
+ await writeFile(settingsPath, serialized, "utf8");
1134
+ process.stderr.write(
1135
+ "\n" + style.dim("Wrote ") + style.cyan(settingsPath) + style.dim(":") + "\n"
1136
+ );
1137
+ process.stderr.write(style.dim(JSON.stringify({ hooks: hooksObj }, null, 2)) + "\n");
1138
+ return true;
1139
+ }
1009
1140
  async function installGeminiCliNativeHooks(ask) {
1010
1141
  const root = multicornShieldPackageRoot();
1011
1142
  const srcBefore = join(root, "plugins", "gemini-cli", "hooks", "scripts", "before-tool.cjs");
@@ -1141,54 +1272,48 @@ function getClaudeDesktopConfigPath() {
1141
1272
  }
1142
1273
  }
1143
1274
  var INIT_WIZARD_PLATFORM_REGISTRY = [
1144
- { slug: "openclaw", displayName: "OpenClaw", section: "native", detectable: true },
1145
- { slug: "claude-code", displayName: "Claude Code", section: "native", detectable: true },
1146
- { slug: "windsurf", displayName: "Windsurf", section: "native", detectable: true },
1147
- { slug: "cline", displayName: "Cline", section: "native", detectable: false },
1148
- { slug: "gemini-cli", displayName: "Gemini CLI", section: "native", detectable: false },
1275
+ { slug: "openclaw", displayName: "OpenClaw", section: "native" },
1276
+ { slug: "claude-code", displayName: "Claude Code", section: "native" },
1277
+ { slug: "windsurf", displayName: "Windsurf", section: "native" },
1278
+ { slug: "cline", displayName: "Cline", section: "native" },
1279
+ { slug: "gemini-cli", displayName: "Gemini CLI", section: "native" },
1149
1280
  {
1150
1281
  slug: "cursor",
1151
1282
  displayName: "Cursor",
1152
1283
  section: "hosted",
1153
- prereqUrl: "https://www.cursor.com/downloads",
1154
- detectable: true
1284
+ prereqUrl: "https://www.cursor.com/downloads"
1155
1285
  },
1156
1286
  {
1157
1287
  slug: "claude-desktop",
1158
1288
  displayName: "Claude Desktop",
1159
1289
  section: "hosted",
1160
- prereqUrl: "https://claude.ai/download",
1161
- detectable: false
1290
+ prereqUrl: "https://claude.ai/download"
1162
1291
  },
1163
1292
  {
1164
1293
  slug: "github-copilot",
1165
1294
  displayName: "GitHub Copilot",
1166
1295
  section: "hosted",
1167
- prereqUrl: "https://docs.github.com/en/copilot/get-started",
1168
- detectable: false
1296
+ prereqUrl: "https://docs.github.com/en/copilot/get-started"
1169
1297
  },
1170
1298
  {
1171
1299
  slug: "kilo-code",
1172
1300
  displayName: "Kilo Code",
1173
1301
  section: "hosted",
1174
- prereqUrl: "https://kilocode.ai/docs/getting-started",
1175
- detectable: false
1302
+ prereqUrl: "https://kilocode.ai/docs/getting-started"
1176
1303
  },
1177
1304
  {
1178
1305
  slug: "continue-dev",
1179
1306
  displayName: "Continue",
1180
1307
  section: "hosted",
1181
- prereqUrl: "https://docs.continue.dev/ide-extensions/install",
1182
- detectable: false
1308
+ prereqUrl: "https://docs.continue.dev/ide-extensions/install"
1183
1309
  },
1184
1310
  {
1185
1311
  slug: "goose",
1186
1312
  displayName: "Goose",
1187
1313
  section: "hosted",
1188
- prereqUrl: "https://goose-docs.ai/docs/quickstart/",
1189
- detectable: false
1314
+ prereqUrl: "https://goose-docs.ai/docs/quickstart/"
1190
1315
  },
1191
- { slug: "other-mcp", displayName: "Local MCP / Other", section: "hosted", detectable: false }
1316
+ { slug: "other-mcp", displayName: "Local MCP / Other", section: "hosted" }
1192
1317
  ];
1193
1318
  var INIT_WIZARD_MENU_SECTIONS = (() => {
1194
1319
  const itemsFor = (section) => INIT_WIZARD_PLATFORM_REGISTRY.filter((e) => e.section === section).map((e) => ({
@@ -1219,45 +1344,17 @@ async function promptHostedProxyInstallPrereq(ask, platformLabel, prereqUrl) {
1219
1344
  const answer = await ask("Ready to continue? (Y/n) ");
1220
1345
  return answer.trim().toLowerCase() !== "n";
1221
1346
  }
1222
- function isPlatformDetectedForMenu(slug) {
1223
- switch (slug) {
1224
- case "openclaw":
1225
- return isOpenClawConnected();
1226
- case "claude-code":
1227
- return Promise.resolve(isClaudeCodeConnected());
1228
- case "cursor":
1229
- return isCursorConnected();
1230
- case "windsurf":
1231
- return isWindsurfConnected();
1232
- default:
1233
- return Promise.resolve(false);
1234
- }
1235
- }
1236
1347
  async function promptPlatformSelection(ask) {
1237
1348
  process.stderr.write(
1238
1349
  "\n" + style.bold(style.violet("Which platform are you connecting?")) + "\n\n"
1239
1350
  );
1240
- const detectionSlugs = INIT_WIZARD_PLATFORM_REGISTRY.filter((e) => e.detectable).map(
1241
- (e) => e.slug
1242
- );
1243
- const connectedFlags = await Promise.all(
1244
- detectionSlugs.map((slug) => isPlatformDetectedForMenu(slug))
1245
- );
1246
- function markerFor(platform) {
1247
- const idx = detectionSlugs.indexOf(platform);
1248
- if (idx === -1) return "";
1249
- if (!connectedFlags[idx]) return "";
1250
- return " " + style.dim("\u25CF detected locally");
1251
- }
1252
1351
  let optionNum = 1;
1253
1352
  for (const section of INIT_WIZARD_MENU_SECTIONS) {
1254
1353
  process.stderr.write(" " + style.dim(section.title) + "\n");
1255
1354
  for (const item of section.items) {
1256
1355
  const indent = optionNum >= 10 ? " " : " ";
1257
- process.stderr.write(
1258
- `${indent}${style.violet(String(optionNum))}. ${item.label}${markerFor(item.platform)}
1259
- `
1260
- );
1356
+ process.stderr.write(`${indent}${style.violet(String(optionNum))}. ${item.label}
1357
+ `);
1261
1358
  optionNum++;
1262
1359
  }
1263
1360
  }
@@ -1720,6 +1817,7 @@ async function runInit(explicitBaseUrl) {
1720
1817
  spinner.stop(true, "Key validated");
1721
1818
  apiKey = key;
1722
1819
  }
1820
+ await warnIfInstalledShieldIsOutdated();
1723
1821
  const configuredAgents = [];
1724
1822
  let currentAgents = collectAgentsFromConfig(existing);
1725
1823
  let lastConfig = {
@@ -1791,63 +1889,41 @@ async function runInit(explicitBaseUrl) {
1791
1889
  ) + "\n"
1792
1890
  );
1793
1891
  if (agentsForPlatform.length > 0) {
1794
- const exactForWorkspace = agentsForPlatform.find(
1795
- (a) => typeof a.workspacePath === "string" && a.workspacePath.length > 0 && resolve(a.workspacePath) === initWorkspacePath
1796
- );
1797
- if (exactForWorkspace !== void 0) {
1798
- process.stderr.write(
1799
- `
1800
- This workspace already has a ${selectedLabel} agent registered (${style.cyan(
1801
- exactForWorkspace.name
1802
- )}).
1803
- `
1804
- );
1805
- process.stderr.write(
1806
- style.dim(
1807
- "Replace updates this directory's saved agent. (n) returns to platform selection \u2014 the wizard keeps running."
1808
- ) + "\n"
1809
- );
1810
- const replace = await ask("Replace it? (Y/n) ");
1811
- if (replace.trim().toLowerCase() === "n") {
1812
- process.stderr.write(style.dim("Skipping. Returning to platform selection.") + "\n");
1813
- continue;
1814
- }
1815
- removeAgentNameBeforeSave = exactForWorkspace.name;
1816
- } else {
1817
- process.stderr.write(
1818
- `
1892
+ process.stderr.write(
1893
+ `
1819
1894
  You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLabel}:
1820
1895
  `
1821
- );
1822
- for (const a of agentsForPlatform) {
1823
- const wsHint = typeof a.workspacePath === "string" && a.workspacePath.length > 0 ? ` ${style.dim(a.workspacePath)}` : "";
1824
- process.stderr.write(` ${style.dim("\u2022")} ${style.cyan(a.name)}${wsHint}
1896
+ );
1897
+ for (const a of agentsForPlatform) {
1898
+ const isThisWorkspace = typeof a.workspacePath === "string" && a.workspacePath.length > 0 && resolve(a.workspacePath) === initWorkspacePath;
1899
+ const wsHint = typeof a.workspacePath === "string" && a.workspacePath.length > 0 ? ` ${style.dim(a.workspacePath)}` : "";
1900
+ const marker = isThisWorkspace ? ` ${style.yellow("(this workspace)")}` : "";
1901
+ process.stderr.write(` ${style.dim("\u2022")} ${style.cyan(a.name)}${wsHint}${marker}
1825
1902
  `);
1826
- }
1827
- process.stderr.write("\n" + style.bold("What would you like to do?") + "\n");
1828
- const actionIdx = await arrowSelect(
1829
- [
1830
- "Add a new agent alongside these",
1831
- "Replace an existing agent",
1832
- "Skip \u2014 choose a different platform"
1833
- ],
1903
+ }
1904
+ process.stderr.write("\n" + style.bold("What would you like to do?") + "\n");
1905
+ const actionIdx = await arrowSelect(
1906
+ [
1907
+ "Add a new agent alongside these",
1908
+ "Replace an existing agent",
1909
+ "Skip - choose a different platform"
1910
+ ],
1911
+ ask,
1912
+ "Action"
1913
+ );
1914
+ if (actionIdx === 2) {
1915
+ continue;
1916
+ }
1917
+ if (actionIdx === 1) {
1918
+ process.stderr.write("\n" + style.bold("Which agent to replace?") + "\n");
1919
+ const replaceIdx = await arrowSelect(
1920
+ agentsForPlatform.map((a) => a.name),
1834
1921
  ask,
1835
- "Action"
1922
+ "Agent"
1836
1923
  );
1837
- if (actionIdx === 2) {
1838
- continue;
1839
- }
1840
- if (actionIdx === 1) {
1841
- process.stderr.write("\n" + style.bold("Which agent to replace?") + "\n");
1842
- const replaceIdx = await arrowSelect(
1843
- agentsForPlatform.map((a) => a.name),
1844
- ask,
1845
- "Agent"
1846
- );
1847
- const victim = agentsForPlatform[replaceIdx];
1848
- if (victim !== void 0) {
1849
- removeAgentNameBeforeSave = victim.name;
1850
- }
1924
+ const victim = agentsForPlatform[replaceIdx];
1925
+ if (victim !== void 0) {
1926
+ removeAgentNameBeforeSave = victim.name;
1851
1927
  }
1852
1928
  }
1853
1929
  }
@@ -1949,16 +2025,24 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
1949
2025
  });
1950
2026
  setupSucceeded = true;
1951
2027
  } else if (selectedPlatform === "claude-code") {
1952
- process.stderr.write("\nTo connect Claude Code to Shield:\n\n");
1953
- process.stderr.write(
1954
- " " + style.bold("Step 1") + " - Add the Multicorn marketplace:\n " + style.cyan("claude plugin marketplace add Multicorn-AI/multicorn-shield") + "\n\n"
1955
- );
1956
2028
  process.stderr.write(
1957
- " " + style.bold("Step 2") + " - Install the plugin:\n " + style.cyan("claude plugin install multicorn-shield@multicorn-shield") + "\n\n"
2029
+ "\n" + style.dim("Configuring Shield hooks in Claude Code user settings...") + "\n"
1958
2030
  );
2031
+ const hooksOk = await installClaudeCodeUserSettingsHooks(ask);
2032
+ if (!hooksOk) {
2033
+ process.stderr.write(style.dim("Skipped Claude Code hook installation.\n"));
2034
+ continue;
2035
+ }
1959
2036
  process.stderr.write(
1960
- style.dim("Requires Claude Code to be installed. Get it at https://code.claude.com") + "\n"
2037
+ style.green("\u2713") + " Shield hooks added to " + style.cyan("~/.claude/settings.json") + "\n"
1961
2038
  );
2039
+ if (claudeInstalledPluginsListsMulticornShield()) {
2040
+ process.stderr.write(
2041
+ style.dim(
2042
+ "Note: You have the multicorn-shield Claude Code plugin installed. The plugin is no longer needed - hooks are now written directly to settings.json. You can uninstall it with: "
2043
+ ) + style.cyan("claude plugin uninstall multicorn-shield@multicorn-shield") + "\n"
2044
+ );
2045
+ }
1962
2046
  configuredAgents.push({
1963
2047
  selection,
1964
2048
  platform: selectedPlatform,
@@ -2313,42 +2397,42 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
2313
2397
  const blocks = [];
2314
2398
  if (configuredPlatforms.has("openclaw")) {
2315
2399
  blocks.push(
2316
- "\n" + style.bold("To complete your OpenClaw setup:") + "\n \u2192 Restart your gateway: " + style.cyan("openclaw gateway restart") + "\n \u2192 Start a session: " + style.cyan("openclaw tui") + "\n"
2400
+ "\n" + style.bold("OpenClaw") + "\n \u2192 Restart your gateway: " + style.cyan("openclaw gateway restart") + "\n \u2192 Start a session: " + style.cyan("openclaw tui") + "\n"
2317
2401
  );
2318
2402
  }
2319
2403
  if (configuredPlatforms.has("claude-code")) {
2320
2404
  blocks.push(
2321
- "\n" + style.bold("To complete your Claude Code setup:") + "\n \u2192 Add marketplace: " + style.cyan("claude plugin marketplace add Multicorn-AI/multicorn-shield") + "\n \u2192 Install plugin: " + style.cyan("claude plugin install multicorn-shield@multicorn-shield") + "\n"
2405
+ "\n" + style.bold("Claude Code") + "\n \u2192 Start Claude Code: " + style.cyan("claude") + "\n \u2192 Shield will intercept tool calls automatically.\n"
2322
2406
  );
2323
2407
  }
2324
2408
  if (configuredPlatforms.has("claude-desktop")) {
2325
2409
  blocks.push(
2326
- "\n" + style.bold("To complete your Claude Desktop setup:") + "\n \u2192 Restart Claude Desktop to pick up config changes\n"
2410
+ "\n" + style.bold("Claude Desktop") + "\n \u2192 Restart Claude Desktop to pick up config changes\n"
2327
2411
  );
2328
2412
  }
2329
2413
  if (configuredPlatforms.has("cursor")) {
2330
2414
  blocks.push(
2331
- "\n" + style.bold("To complete your Cursor setup:") + "\n 1. If you don't have Cursor yet, download it from " + style.cyan("https://cursor.com/downloads") + "\n 2. Open " + style.cyan("~/.cursor/mcp.json") + " and paste the config snippet shown above\n 3. Restart Cursor (or launch it for the first time) to load the new MCP server\n"
2415
+ "\n" + style.bold("Cursor") + "\n \u2192 If needed, download Cursor from " + style.cyan("https://cursor.com/downloads") + "\n \u2192 Restart Cursor so it loads the MCP server\n \u2192 Ask the agent to use your Shield MCP tools by short name\n"
2332
2416
  );
2333
2417
  }
2334
2418
  if (configuredPlatforms.has("kilo-code")) {
2335
2419
  blocks.push(
2336
- "\n" + style.bold("To complete your Kilo Code setup:") + "\n 1. Save the snippet to " + style.cyan(".kilocode/mcp.json") + " in your project root, or under the mcp key in " + style.cyan("kilo.jsonc") + "\n 2. Run your next task in Kilo Code so it picks up the MCP server\n"
2420
+ "\n" + style.bold("Kilo Code") + "\n \u2192 Restart the editor or reload the window if the MCP server does not appear\n \u2192 Run your next task in Kilo Code so it picks up Shield\n"
2337
2421
  );
2338
2422
  }
2339
2423
  if (configuredPlatforms.has("github-copilot")) {
2340
2424
  blocks.push(
2341
- "\n" + style.bold("GitHub Copilot MCP:") + "\n 1. Open VS Code Command Palette: Preferences: Open User Settings (JSON)\n 2. Merge the snippet under the " + style.cyan("mcp") + " key and save\n 3. Use Copilot Agent mode and verify the MCP server connects\n"
2425
+ "\n" + style.bold("GitHub Copilot") + "\n \u2192 Reload the editor window if the MCP server does not appear\n \u2192 Use Copilot Agent mode and verify the MCP server connects\n"
2342
2426
  );
2343
2427
  }
2344
2428
  if (configuredPlatforms.has("continue-dev")) {
2345
2429
  blocks.push(
2346
- "\n" + style.bold("Continue MCP:") + "\n 1. If you don't have Continue yet, install from " + style.cyan("https://docs.continue.dev/ide-extensions/install") + "\n 2. Save JSON as " + style.cyan(".continue/mcpServers/shield.json") + " in your workspace, or add to " + style.cyan("~/.continue/config.yaml") + "\n 3. Reload VS Code and open Continue agent mode\n"
2430
+ "\n" + style.bold("Continue") + "\n \u2192 If needed, install Continue from " + style.cyan("https://docs.continue.dev/ide-extensions/install") + "\n \u2192 Reload VS Code and open Continue agent mode\n"
2347
2431
  );
2348
2432
  }
2349
2433
  if (configuredPlatforms.has("goose")) {
2350
2434
  blocks.push(
2351
- "\n" + style.bold("Goose MCP extension:") + "\n 1. Edit " + style.cyan("~/.config/goose/config.yaml") + " (or use goose configure)\n 2. Restart Goose CLI or Desktop\n"
2435
+ "\n" + style.bold("Goose") + "\n \u2192 Start a new Goose session after updating config\n"
2352
2436
  );
2353
2437
  }
2354
2438
  const windsurfNativeConfigured = configuredAgents.some(
@@ -2359,12 +2443,12 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
2359
2443
  );
2360
2444
  if (windsurfNativeConfigured) {
2361
2445
  blocks.push(
2362
- "\n" + style.bold("To complete native Windsurf (Shield) setup:") + "\n 1. Hook scripts: " + style.cyan(getWindsurfHooksInstallDir()) + "\n 2. Hooks config: " + style.cyan(getWindsurfCascadeHooksJsonPath()) + "\n 3. Restart Windsurf (quit fully, then reopen)\n"
2446
+ "\n" + style.bold("Windsurf (native)") + "\n \u2192 Restart Windsurf (quit fully, then reopen)\n"
2363
2447
  );
2364
2448
  }
2365
2449
  if (windsurfHostedConfigured) {
2366
2450
  blocks.push(
2367
- "\n" + style.bold("To complete your Windsurf hosted-proxy setup:") + "\n 1. If you don't have Windsurf yet, download it from " + style.cyan("https://windsurf.com/download") + "\n 2. Open " + style.cyan("~/.codeium/windsurf/mcp_config.json") + " and paste the config snippet shown above\n 3. Restart Windsurf (or launch it for the first time) to load the new MCP server\n"
2451
+ "\n" + style.bold("Windsurf (hosted)") + "\n \u2192 If needed, install from " + style.cyan("https://windsurf.com/download") + "\n \u2192 Restart Windsurf so it loads the MCP server\n"
2368
2452
  );
2369
2453
  }
2370
2454
  const clineNativeConfigured = configuredAgents.some(
@@ -2375,12 +2459,12 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
2375
2459
  );
2376
2460
  if (clineNativeConfigured) {
2377
2461
  blocks.push(
2378
- "\n" + style.bold("To complete native Cline (Shield) setup:") + "\n 1. Enable Hooks in Cline: open VS Code, click the Cline sidebar icon, click the gear icon,\n scroll down to the Advanced section, and toggle Hooks on.\n 2. Reload the VS Code window (Cmd+Shift+P > Reload Window)\n 3. Trigger any tool call to verify Shield is intercepting\n"
2462
+ "\n" + style.bold("Cline (native)") + "\n \u2192 Enable Hooks in Cline settings (Advanced), then reload the VS Code window\n \u2192 Trigger a tool call to verify Shield is intercepting\n"
2379
2463
  );
2380
2464
  }
2381
2465
  if (clineHostedConfigured) {
2382
2466
  blocks.push(
2383
- "\n" + style.bold("To complete your Cline hosted-proxy setup:") + "\n 1. If you don't have Cline yet, install it from the VS Code marketplace\n 2. Open your Cline MCP settings file and paste the config snippet shown above\n 3. Restart Cline or reload the VS Code window\n"
2467
+ "\n" + style.bold("Cline (hosted)") + "\n \u2192 Restart Cline or reload the VS Code window\n"
2384
2468
  );
2385
2469
  }
2386
2470
  const geminiCliNativeConfigured = configuredAgents.some(
@@ -2391,12 +2475,12 @@ You have ${String(agentsForPlatform.length)} agent(s) connected for ${selectedLa
2391
2475
  );
2392
2476
  if (geminiCliNativeConfigured) {
2393
2477
  blocks.push(
2394
- "\n" + style.bold("Gemini CLI native hooks:") + "\n Your Gemini CLI hooks are installed. Restart Gemini CLI to activate Shield governance.\n"
2478
+ "\n" + style.bold("Gemini CLI (native)") + "\n \u2192 Restart Gemini CLI to activate Shield governance\n"
2395
2479
  );
2396
2480
  }
2397
2481
  if (geminiCliHostedConfigured) {
2398
2482
  blocks.push(
2399
- "\n" + style.bold("To complete your Gemini CLI setup:") + "\n 1. Open " + style.cyan("~/.gemini/settings.json") + "\n 2. Paste the config snippet shown above\n 3. Restart Gemini CLI, then run /mcp to verify\n"
2483
+ "\n" + style.bold("Gemini CLI (hosted)") + "\n \u2192 Restart Gemini CLI, then run " + style.cyan("/mcp") + " to verify the server\n"
2400
2484
  );
2401
2485
  }
2402
2486
  if (blocks.length > 0) {
@@ -45,9 +45,9 @@ var TOOL_MAP = {
45
45
  slack_read: { service: "slack", permissionLevel: "read" },
46
46
  slack_message: { service: "slack", permissionLevel: "write" },
47
47
  // Payments
48
- payments: { service: "payments", permissionLevel: "execute" },
49
- payment: { service: "payments", permissionLevel: "execute" },
50
- stripe: { service: "payments", permissionLevel: "execute" }
48
+ payments: { service: "payments", permissionLevel: "write" },
49
+ payment: { service: "payments", permissionLevel: "write" },
50
+ stripe: { service: "payments", permissionLevel: "write" }
51
51
  };
52
52
  function mapToolToScope(toolName, command) {
53
53
  const normalized = toolName.trim().toLowerCase();