@scopebond/hook 0.6.0 → 0.7.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 (46) hide show
  1. package/README.md +87 -6
  2. package/dist/cli.d.ts +2 -1
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +612 -70
  5. package/dist/cli.js.map +1 -1
  6. package/dist/cloud.d.ts +2 -0
  7. package/dist/cloud.d.ts.map +1 -1
  8. package/dist/cloud.js +1 -1
  9. package/dist/cloud.js.map +1 -1
  10. package/dist/explain.d.ts +44 -0
  11. package/dist/explain.d.ts.map +1 -0
  12. package/dist/explain.js +75 -0
  13. package/dist/explain.js.map +1 -0
  14. package/dist/index.d.ts +8 -2
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +4 -1
  17. package/dist/index.js.map +1 -1
  18. package/dist/init.d.ts +9 -3
  19. package/dist/init.d.ts.map +1 -1
  20. package/dist/init.js +35 -20
  21. package/dist/init.js.map +1 -1
  22. package/dist/install.d.ts +45 -2
  23. package/dist/install.d.ts.map +1 -1
  24. package/dist/install.js +158 -9
  25. package/dist/install.js.map +1 -1
  26. package/dist/map.d.ts +6 -0
  27. package/dist/map.d.ts.map +1 -1
  28. package/dist/map.js +4 -1
  29. package/dist/map.js.map +1 -1
  30. package/dist/rules.d.ts +51 -0
  31. package/dist/rules.d.ts.map +1 -0
  32. package/dist/rules.js +216 -0
  33. package/dist/rules.js.map +1 -0
  34. package/dist/runtime-install.d.ts +25 -0
  35. package/dist/runtime-install.d.ts.map +1 -0
  36. package/dist/runtime-install.js +106 -0
  37. package/dist/runtime-install.js.map +1 -0
  38. package/dist/runtime.d.ts +13 -0
  39. package/dist/runtime.d.ts.map +1 -1
  40. package/dist/runtime.js +52 -7
  41. package/dist/runtime.js.map +1 -1
  42. package/dist/version.d.ts +5 -1
  43. package/dist/version.d.ts.map +1 -1
  44. package/dist/version.js +6 -2
  45. package/dist/version.js.map +1 -1
  46. package/package.json +3 -3
package/dist/index.d.ts CHANGED
@@ -2,9 +2,15 @@ export { mapClaudeToolUse, mapCodexToolUse, mapCursorEvent, fillPushBranch } fro
2
2
  export type { Mapped, NormalizedIntent } from "./map.js";
3
3
  export { createHookRuntime, starterPolicy, upgradeStarterPolicy } from "./runtime.js";
4
4
  export type { RuntimeConfig, Decision } from "./runtime.js";
5
+ export { compile, defaultRules, describeRules, loadRules, saveRules, rulesPath, pathRuleFor, RULES_FILE, } from "./rules.js";
6
+ export type { RuleSet, PathRule } from "./rules.js";
5
7
  export { scaffold, harnessSnippet, installHarness } from "./init.js";
6
- export { userHome, userHarnessFile, resolveConfigDir, writeHarnessConfig, removeHarnessConfig, cursorDetected, codexDetected, absoluteHookCommand, isHarnessConfigured, purgeHome, readHarnessConfig, trustProjectPolicy, isTrustedProject, untrustedProjectPolicy, trustedProjectsFile, } from "./install.js";
7
- export type { Harness } from "./install.js";
8
+ export { userHome, userHarnessFile, projectHarnessFile, resolveConfigDir, writeHarnessConfig, removeHarnessConfig, cursorDetected, codexDetected, absoluteHookCommand, isHarnessConfigured, purgeHome, readHarnessConfig, trustProjectPolicy, isTrustedProject, untrustedProjectPolicy, trustedProjectsFile, harnessScopes, harnessScopeLabel, configuredHookCommands, hookCommandResolves, isScopebondHookCommand, harnessEntryMatches, } from "./install.js";
9
+ export type { Harness, HarnessScopes } from "./install.js";
10
+ export { explainDeny, describeAction, findClause } from "./explain.js";
11
+ export type { ExplainDenyInput, ExplainIntent, ExplainPolicy, ExplainClause } from "./explain.js";
12
+ export { ensureDurableRuntime, isEphemeralPath, nodeModulesRootOf, runtimeRoot, runtimeDirFor, pinnedCliPath, } from "./runtime-install.js";
13
+ export type { PinResult } from "./runtime-install.js";
8
14
  export { connectCloud, loadConnection, attachExporter, flushBounded, connectionPath } from "./cloud.js";
9
15
  export type { HookConnection } from "./cloud.js";
10
16
  export { scrubSecrets, scrubParam, redactCommand, digest, sha256 } from "./minimize.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC7F,YAAY,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AACzD,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AACtF,YAAY,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AACrE,OAAO,EACL,QAAQ,EAAE,eAAe,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,mBAAmB,EACpF,cAAc,EAAE,aAAa,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,SAAS,EAClF,iBAAiB,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,mBAAmB,GACrG,MAAM,cAAc,CAAC;AACtB,YAAY,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAC5C,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,cAAc,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AACxG,YAAY,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAIA,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAC7F,YAAY,EAAE,MAAM,EAAE,gBAAgB,EAAE,MAAM,UAAU,CAAC;AACzD,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AACtF,YAAY,EAAE,aAAa,EAAE,QAAQ,EAAE,MAAM,cAAc,CAAC;AAC5D,OAAO,EACL,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,WAAW,EAAE,UAAU,GAC/F,MAAM,YAAY,CAAC;AACpB,YAAY,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,YAAY,CAAC;AACpD,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AACrE,OAAO,EACL,QAAQ,EAAE,eAAe,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,mBAAmB,EACxG,cAAc,EAAE,aAAa,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,SAAS,EAClF,iBAAiB,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,mBAAmB,EACpG,aAAa,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,mBAAmB,EAC7E,sBAAsB,EAAE,mBAAmB,GAC5C,MAAM,cAAc,CAAC;AACtB,YAAY,EAAE,OAAO,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAC3D,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AACvE,YAAY,EAAE,gBAAgB,EAAE,aAAa,EAAE,aAAa,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAClG,OAAO,EACL,oBAAoB,EAAE,eAAe,EAAE,iBAAiB,EAAE,WAAW,EAAE,aAAa,EAAE,aAAa,GACpG,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACtD,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,cAAc,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AACxG,YAAY,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AACjD,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC"}
package/dist/index.js CHANGED
@@ -3,8 +3,11 @@
3
3
  // binary is the thin harness adapter over them.
4
4
  export { mapClaudeToolUse, mapCodexToolUse, mapCursorEvent, fillPushBranch } from "./map.js";
5
5
  export { createHookRuntime, starterPolicy, upgradeStarterPolicy } from "./runtime.js";
6
+ export { compile, defaultRules, describeRules, loadRules, saveRules, rulesPath, pathRuleFor, RULES_FILE, } from "./rules.js";
6
7
  export { scaffold, harnessSnippet, installHarness } from "./init.js";
7
- export { userHome, userHarnessFile, resolveConfigDir, writeHarnessConfig, removeHarnessConfig, cursorDetected, codexDetected, absoluteHookCommand, isHarnessConfigured, purgeHome, readHarnessConfig, trustProjectPolicy, isTrustedProject, untrustedProjectPolicy, trustedProjectsFile, } from "./install.js";
8
+ export { userHome, userHarnessFile, projectHarnessFile, resolveConfigDir, writeHarnessConfig, removeHarnessConfig, cursorDetected, codexDetected, absoluteHookCommand, isHarnessConfigured, purgeHome, readHarnessConfig, trustProjectPolicy, isTrustedProject, untrustedProjectPolicy, trustedProjectsFile, harnessScopes, harnessScopeLabel, configuredHookCommands, hookCommandResolves, isScopebondHookCommand, harnessEntryMatches, } from "./install.js";
9
+ export { explainDeny, describeAction, findClause } from "./explain.js";
10
+ export { ensureDurableRuntime, isEphemeralPath, nodeModulesRootOf, runtimeRoot, runtimeDirFor, pinnedCliPath, } from "./runtime-install.js";
8
11
  export { connectCloud, loadConnection, attachExporter, flushBounded, connectionPath } from "./cloud.js";
9
12
  export { scrubSecrets, scrubParam, redactCommand, digest, sha256 } from "./minimize.js";
10
13
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,sFAAsF;AACtF,4EAA4E;AAC5E,gDAAgD;AAEhD,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAE7F,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAEtF,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AACrE,OAAO,EACL,QAAQ,EAAE,eAAe,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,mBAAmB,EACpF,cAAc,EAAE,aAAa,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,SAAS,EAClF,iBAAiB,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,mBAAmB,GACrG,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,cAAc,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAExG,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC","sourcesContent":["// @scopebond/hook — the Claude Code, Cursor + Codex connector. The library surface is\n// the deterministic mapper and the check-only runtime; the `scopebond-hook`\n// binary is the thin harness adapter over them.\n\nexport { mapClaudeToolUse, mapCodexToolUse, mapCursorEvent, fillPushBranch } from \"./map.js\";\nexport type { Mapped, NormalizedIntent } from \"./map.js\";\nexport { createHookRuntime, starterPolicy, upgradeStarterPolicy } from \"./runtime.js\";\nexport type { RuntimeConfig, Decision } from \"./runtime.js\";\nexport { scaffold, harnessSnippet, installHarness } from \"./init.js\";\nexport {\n userHome, userHarnessFile, resolveConfigDir, writeHarnessConfig, removeHarnessConfig,\n cursorDetected, codexDetected, absoluteHookCommand, isHarnessConfigured, purgeHome,\n readHarnessConfig, trustProjectPolicy, isTrustedProject, untrustedProjectPolicy, trustedProjectsFile,\n} from \"./install.js\";\nexport type { Harness } from \"./install.js\";\nexport { connectCloud, loadConnection, attachExporter, flushBounded, connectionPath } from \"./cloud.js\";\nexport type { HookConnection } from \"./cloud.js\";\nexport { scrubSecrets, scrubParam, redactCommand, digest, sha256 } from \"./minimize.js\";\n"]}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,sFAAsF;AACtF,4EAA4E;AAC5E,gDAAgD;AAEhD,OAAO,EAAE,gBAAgB,EAAE,eAAe,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,UAAU,CAAC;AAE7F,OAAO,EAAE,iBAAiB,EAAE,aAAa,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAC;AAEtF,OAAO,EACL,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,WAAW,EAAE,UAAU,GAC/F,MAAM,YAAY,CAAC;AAEpB,OAAO,EAAE,QAAQ,EAAE,cAAc,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AACrE,OAAO,EACL,QAAQ,EAAE,eAAe,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,kBAAkB,EAAE,mBAAmB,EACxG,cAAc,EAAE,aAAa,EAAE,mBAAmB,EAAE,mBAAmB,EAAE,SAAS,EAClF,iBAAiB,EAAE,kBAAkB,EAAE,gBAAgB,EAAE,sBAAsB,EAAE,mBAAmB,EACpG,aAAa,EAAE,iBAAiB,EAAE,sBAAsB,EAAE,mBAAmB,EAC7E,sBAAsB,EAAE,mBAAmB,GAC5C,MAAM,cAAc,CAAC;AAEtB,OAAO,EAAE,WAAW,EAAE,cAAc,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAEvE,OAAO,EACL,oBAAoB,EAAE,eAAe,EAAE,iBAAiB,EAAE,WAAW,EAAE,aAAa,EAAE,aAAa,GACpG,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAAE,YAAY,EAAE,cAAc,EAAE,cAAc,EAAE,YAAY,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAExG,OAAO,EAAE,YAAY,EAAE,UAAU,EAAE,aAAa,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,eAAe,CAAC","sourcesContent":["// @scopebond/hook — the Claude Code, Cursor + Codex connector. The library surface is\n// the deterministic mapper and the check-only runtime; the `scopebond-hook`\n// binary is the thin harness adapter over them.\n\nexport { mapClaudeToolUse, mapCodexToolUse, mapCursorEvent, fillPushBranch } from \"./map.js\";\nexport type { Mapped, NormalizedIntent } from \"./map.js\";\nexport { createHookRuntime, starterPolicy, upgradeStarterPolicy } from \"./runtime.js\";\nexport type { RuntimeConfig, Decision } from \"./runtime.js\";\nexport {\n compile, defaultRules, describeRules, loadRules, saveRules, rulesPath, pathRuleFor, RULES_FILE,\n} from \"./rules.js\";\nexport type { RuleSet, PathRule } from \"./rules.js\";\nexport { scaffold, harnessSnippet, installHarness } from \"./init.js\";\nexport {\n userHome, userHarnessFile, projectHarnessFile, resolveConfigDir, writeHarnessConfig, removeHarnessConfig,\n cursorDetected, codexDetected, absoluteHookCommand, isHarnessConfigured, purgeHome,\n readHarnessConfig, trustProjectPolicy, isTrustedProject, untrustedProjectPolicy, trustedProjectsFile,\n harnessScopes, harnessScopeLabel, configuredHookCommands, hookCommandResolves,\n isScopebondHookCommand, harnessEntryMatches,\n} from \"./install.js\";\nexport type { Harness, HarnessScopes } from \"./install.js\";\nexport { explainDeny, describeAction, findClause } from \"./explain.js\";\nexport type { ExplainDenyInput, ExplainIntent, ExplainPolicy, ExplainClause } from \"./explain.js\";\nexport {\n ensureDurableRuntime, isEphemeralPath, nodeModulesRootOf, runtimeRoot, runtimeDirFor, pinnedCliPath,\n} from \"./runtime-install.js\";\nexport type { PinResult } from \"./runtime-install.js\";\nexport { connectCloud, loadConnection, attachExporter, flushBounded, connectionPath } from \"./cloud.js\";\nexport type { HookConnection } from \"./cloud.js\";\nexport { scrubSecrets, scrubParam, redactCommand, digest, sha256 } from \"./minimize.js\";\n"]}
package/dist/init.d.ts CHANGED
@@ -3,12 +3,18 @@ export declare function scaffold(dir: string, opts?: {
3
3
  }): {
4
4
  agentKid: string;
5
5
  policyPath: string;
6
+ rulesPath: string;
6
7
  };
7
8
  /** Install the hook into the agent's own config file, merging with anything already
8
9
  * there (idempotent — safe to run twice). Returns the file it wrote. This is what
9
10
  * lets `connect` finish setup without the user hand-editing JSON.
10
11
  * `cwd` defaults to the current directory (the project root the user runs it in). */
11
- export declare function installHarness(harness: "claude" | "cursor" | "codex", cwd?: string): string;
12
- /** The harness configuration snippet to install the hook (version-pinned npx). */
13
- export declare function harnessSnippet(harness: "claude" | "cursor" | "codex"): string;
12
+ export declare function installHarness(harness: "claude" | "cursor" | "codex", cwd?: string,
13
+ /** The command to run per tool call. Defaults to the portable `npx` form; `init`
14
+ * passes a pinned absolute path when it has one, which is ~7x faster to start. */
15
+ command?: string): string;
16
+ /** The harness configuration snippet to install the hook by hand. Defaults to the
17
+ * version-pinned `npx` form; `init --no-install` passes the pinned path it chose, so
18
+ * the printed snippet matches what `init` would have written. */
19
+ export declare function harnessSnippet(harness: "claude" | "cursor" | "codex", command?: string): string;
14
20
  //# sourceMappingURL=init.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"AAYA,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAgB9G;AAED;;;sFAGsF;AACtF,wBAAgB,cAAc,CAAC,OAAO,EAAE,QAAQ,GAAG,QAAQ,GAAG,OAAO,EAAE,GAAG,GAAE,MAAsB,GAAG,MAAM,CAkC1G;AAED,kFAAkF;AAClF,wBAAgB,cAAc,CAAC,OAAO,EAAE,QAAQ,GAAG,QAAQ,GAAG,OAAO,GAAG,MAAM,CA2B7E"}
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"AAYA,wBAAgB,QAAQ,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,GAAE;IAAE,KAAK,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG;IAAE,QAAQ,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAA;CAAE,CAuBjI;AAED;;;sFAGsF;AACtF,wBAAgB,cAAc,CAC5B,OAAO,EAAE,QAAQ,GAAG,QAAQ,GAAG,OAAO,EACtC,GAAG,GAAE,MAAsB;AAC3B;mFACmF;AACnF,OAAO,CAAC,EAAE,MAAM,GACf,MAAM,CAmCR;AAED;;kEAEkE;AAClE,wBAAgB,cAAc,CAAC,OAAO,EAAE,QAAQ,GAAG,QAAQ,GAAG,OAAO,EAAE,OAAO,CAAC,EAAE,MAAM,GAAG,MAAM,CA6B/F"}
package/dist/init.js CHANGED
@@ -5,8 +5,8 @@ import { mkdirSync, writeFileSync, existsSync, readFileSync } from "node:fs";
5
5
  import { dirname, join } from "node:path";
6
6
  import { loadOrCreateAttester } from "@scopebond/gateway/node";
7
7
  import { createSigner } from "@scopebond/sdk";
8
- import { starterPolicy } from "./runtime.js";
9
- import { readHarnessConfig } from "./install.js";
8
+ import { compile, defaultRules, loadRules, saveRules, rulesPath } from "./rules.js";
9
+ import { readHarnessConfig, projectHarnessFile, harnessEntryMatches, backupHarnessConfig } from "./install.js";
10
10
  import { hookCommand } from "./version.js";
11
11
  export function scaffold(dir, opts = {}) {
12
12
  mkdirSync(dir, { recursive: true });
@@ -21,30 +21,42 @@ export function scaffold(dir, opts = {}) {
21
21
  loadOrCreateAttester({ file: keyPath });
22
22
  loadOrCreateAttester({ file: attesterPath });
23
23
  const agent = createSigner({ privateKeyPem: readFileSync(keyPath, "utf8") });
24
+ // The editable rule set beside the compiled policy, so "edit the limits" means editing a
25
+ // readable list rather than a 700-character lookahead. `compile(defaultRules())` produces
26
+ // the starter policy's exact patterns — `rules.test.mjs` pins that — so writing either
27
+ // form enforces the same thing.
28
+ const rulesFile = rulesPath(dir);
29
+ if (!existsSync(rulesFile) || opts.force)
30
+ saveRules(dir, defaultRules());
24
31
  if (!existsSync(policyPath) || opts.force) {
25
- writeFileSync(policyPath, JSON.stringify(starterPolicy(agent.kid), null, 2) + "\n");
32
+ const rules = loadRules(dir) ?? defaultRules();
33
+ writeFileSync(policyPath, JSON.stringify(compile(rules, agent.kid), null, 2) + "\n");
26
34
  }
27
- return { agentKid: agent.kid, policyPath };
35
+ return { agentKid: agent.kid, policyPath, rulesPath: rulesFile };
28
36
  }
29
37
  /** Install the hook into the agent's own config file, merging with anything already
30
38
  * there (idempotent — safe to run twice). Returns the file it wrote. This is what
31
39
  * lets `connect` finish setup without the user hand-editing JSON.
32
40
  * `cwd` defaults to the current directory (the project root the user runs it in). */
33
- export function installHarness(harness, cwd = process.cwd()) {
41
+ export function installHarness(harness, cwd = process.cwd(),
42
+ /** The command to run per tool call. Defaults to the portable `npx` form; `init`
43
+ * passes a pinned absolute path when it has one, which is ~7x faster to start. */
44
+ command) {
34
45
  const isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
35
- const file = harness === "cursor" ? join(cwd, ".cursor", "hooks.json")
36
- : harness === "codex" ? join(cwd, ".codex", "hooks.json")
37
- : join(cwd, ".claude", "settings.json");
46
+ const file = projectHarnessFile(harness, cwd);
38
47
  const config = readHarnessConfig(file); // throws, leaving the file intact, if it is not a JSON object
48
+ backupHarnessConfig(file); // keep the user's original beside it before the first change
39
49
  mkdirSync(dirname(file), { recursive: true });
40
50
  const hooks = isRecord(config.hooks) ? config.hooks : (config.hooks = {});
41
- const cursorCmd = hookCommand("cursor");
42
- const claudeCmd = hookCommand("claude");
43
- const codexCmd = hookCommand("codex");
44
- // Match any existing Scopebond hook entry (pinned or a legacy bare `scopebond-hook`)
45
- // so re-running never appends a duplicate and an old bare command is replaced.
46
- const isScopebond = (cmd) => typeof cmd === "string" && /(^|\s)(npx\s+.*)?@scopebond\/hook|(^|\s)scopebond-hook(\s|$)/.test(cmd);
47
- const entryMatches = (e) => isRecord(e) && (isScopebond(e.command) || (Array.isArray(e.hooks) && e.hooks.some((h) => isRecord(h) && isScopebond(h.command))));
51
+ const forHarness = (h) => command ? command.replace(/\b(claude|cursor|codex)$/, h) : hookCommand(h);
52
+ const cursorCmd = forHarness("cursor");
53
+ const claudeCmd = forHarness("claude");
54
+ const codexCmd = forHarness("codex");
55
+ // Match any existing Scopebond hook entry — npx, pinned absolute (either path
56
+ // separator) or a legacy bare `scopebond-hook` — so re-running replaces the entry
57
+ // instead of appending a second one. One shared matcher with `install`: a private
58
+ // copy here missed the pinned Windows form and duplicated the hook.
59
+ const entryMatches = harnessEntryMatches;
48
60
  if (harness === "cursor") {
49
61
  config.version = config.version ?? 1;
50
62
  for (const event of ["beforeShellExecution", "beforeMCPExecution", "beforeReadFile", "afterFileEdit"]) {
@@ -71,10 +83,13 @@ export function installHarness(harness, cwd = process.cwd()) {
71
83
  writeFileSync(file, JSON.stringify(config, null, 2) + "\n");
72
84
  return file;
73
85
  }
74
- /** The harness configuration snippet to install the hook (version-pinned npx). */
75
- export function harnessSnippet(harness) {
86
+ /** The harness configuration snippet to install the hook by hand. Defaults to the
87
+ * version-pinned `npx` form; `init --no-install` passes the pinned path it chose, so
88
+ * the printed snippet matches what `init` would have written. */
89
+ export function harnessSnippet(harness, command) {
90
+ const cmdFor = (h) => command ? command.replace(/\b(claude|cursor|codex)$/, h) : hookCommand(h);
76
91
  if (harness === "cursor") {
77
- const cmd = hookCommand("cursor");
92
+ const cmd = cmdFor("cursor");
78
93
  return JSON.stringify({
79
94
  version: 1,
80
95
  hooks: {
@@ -89,14 +104,14 @@ export function harnessSnippet(harness) {
89
104
  return JSON.stringify({
90
105
  hooks: {
91
106
  PreToolUse: [{
92
- hooks: [{ type: "command", command: hookCommand("codex"), timeout: 30, statusMessage: "Checking this action with Scopebond" }],
107
+ hooks: [{ type: "command", command: cmdFor("codex"), timeout: 30, statusMessage: "Checking this action with Scopebond" }],
93
108
  }],
94
109
  },
95
110
  }, null, 2);
96
111
  }
97
112
  return JSON.stringify({
98
113
  hooks: {
99
- PreToolUse: [{ matcher: "*", hooks: [{ type: "command", command: hookCommand("claude") }] }],
114
+ PreToolUse: [{ matcher: "*", hooks: [{ type: "command", command: cmdFor("claude") }] }],
100
115
  },
101
116
  }, null, 2);
102
117
  }
package/dist/init.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"init.js","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"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","sourcesContent":["// `scopebond-hook init` — scaffold the local enrollment: a machine signing key, a\n// gateway countersigning key and a starter policy. Keys and receipts stay on the\n// machine; no credentials are handled.\n\nimport { mkdirSync, writeFileSync, existsSync, readFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { loadOrCreateAttester } from \"@scopebond/gateway/node\";\nimport { createSigner } from \"@scopebond/sdk\";\nimport { starterPolicy } from \"./runtime.js\";\nimport { readHarnessConfig } from \"./install.js\";\nimport { hookCommand } from \"./version.js\";\n\nexport function scaffold(dir: string, opts: { force?: boolean } = {}): { agentKid: string; policyPath: string } {\n mkdirSync(dir, { recursive: true });\n const keyPath = join(dir, \"agent.key\");\n const attesterPath = join(dir, \"attester.key\");\n const policyPath = join(dir, \"policy.json\");\n // Never let the signing keys, the Cloud credential or the local log be committed.\n const gitignorePath = join(dir, \".gitignore\");\n if (!existsSync(gitignorePath)) writeFileSync(gitignorePath, \"*\\n\");\n // Machine signing key (agent) + gateway countersigning key (attester). Reused if present.\n loadOrCreateAttester({ file: keyPath });\n loadOrCreateAttester({ file: attesterPath });\n const agent = createSigner({ privateKeyPem: readFileSync(keyPath, \"utf8\") });\n if (!existsSync(policyPath) || opts.force) {\n writeFileSync(policyPath, JSON.stringify(starterPolicy(agent.kid), null, 2) + \"\\n\");\n }\n return { agentKid: agent.kid, policyPath };\n}\n\n/** Install the hook into the agent's own config file, merging with anything already\n * there (idempotent — safe to run twice). Returns the file it wrote. This is what\n * lets `connect` finish setup without the user hand-editing JSON.\n * `cwd` defaults to the current directory (the project root the user runs it in). */\nexport function installHarness(harness: \"claude\" | \"cursor\" | \"codex\", cwd: string = process.cwd()): string {\n const isRecord = (v: unknown): v is Record<string, unknown> => typeof v === \"object\" && v !== null && !Array.isArray(v);\n const file = harness === \"cursor\" ? join(cwd, \".cursor\", \"hooks.json\")\n : harness === \"codex\" ? join(cwd, \".codex\", \"hooks.json\")\n : join(cwd, \".claude\", \"settings.json\");\n const config = readHarnessConfig(file); // throws, leaving the file intact, if it is not a JSON object\n mkdirSync(dirname(file), { recursive: true });\n const hooks = isRecord(config.hooks) ? config.hooks : (config.hooks = {});\n const cursorCmd = hookCommand(\"cursor\");\n const claudeCmd = hookCommand(\"claude\");\n const codexCmd = hookCommand(\"codex\");\n // Match any existing Scopebond hook entry (pinned or a legacy bare `scopebond-hook`)\n // so re-running never appends a duplicate and an old bare command is replaced.\n const isScopebond = (cmd: unknown): boolean => typeof cmd === \"string\" && /(^|\\s)(npx\\s+.*)?@scopebond\\/hook|(^|\\s)scopebond-hook(\\s|$)/.test(cmd);\n const entryMatches = (e: unknown): boolean =>\n isRecord(e) && (isScopebond(e.command) || (Array.isArray(e.hooks) && e.hooks.some((h) => isRecord(h) && isScopebond(h.command))));\n if (harness === \"cursor\") {\n config.version = config.version ?? 1;\n for (const event of [\"beforeShellExecution\", \"beforeMCPExecution\", \"beforeReadFile\", \"afterFileEdit\"]) {\n const list = Array.isArray((hooks as Record<string, unknown>)[event]) ? (hooks as Record<string, unknown[]>)[event] : ((hooks as Record<string, unknown[]>)[event] = []);\n const existing = list.findIndex(entryMatches);\n if (existing >= 0) list[existing] = { command: cursorCmd }; else list.push({ command: cursorCmd });\n }\n } else {\n const list = Array.isArray((hooks as Record<string, unknown>).PreToolUse) ? (hooks as Record<string, unknown[]>).PreToolUse : ((hooks as Record<string, unknown[]>).PreToolUse = []);\n const existing = list.findIndex(entryMatches);\n // Codex matchers are regular expressions. No matcher means all supported tools.\n const entry = harness === \"codex\"\n ? { hooks: [{ type: \"command\", command: codexCmd, timeout: 30, statusMessage: \"Checking this action with Scopebond\" }] }\n : { matcher: \"*\", hooks: [{ type: \"command\", command: claudeCmd }] };\n if (existing >= 0) list[existing] = entry; else list.push(entry);\n }\n writeFileSync(file, JSON.stringify(config, null, 2) + \"\\n\");\n return file;\n}\n\n/** The harness configuration snippet to install the hook (version-pinned npx). */\nexport function harnessSnippet(harness: \"claude\" | \"cursor\" | \"codex\"): string {\n if (harness === \"cursor\") {\n const cmd = hookCommand(\"cursor\");\n return JSON.stringify({\n version: 1,\n hooks: {\n beforeShellExecution: [{ command: cmd }],\n beforeMCPExecution: [{ command: cmd }],\n beforeReadFile: [{ command: cmd }],\n afterFileEdit: [{ command: cmd }],\n },\n }, null, 2);\n }\n if (harness === \"codex\") {\n return JSON.stringify({\n hooks: {\n PreToolUse: [{\n hooks: [{ type: \"command\", command: hookCommand(\"codex\"), timeout: 30, statusMessage: \"Checking this action with Scopebond\" }],\n }],\n },\n }, null, 2);\n }\n return JSON.stringify({\n hooks: {\n PreToolUse: [{ matcher: \"*\", hooks: [{ type: \"command\", command: hookCommand(\"claude\") }] }],\n },\n }, null, 2);\n}\n"]}
1
+ {"version":3,"file":"init.js","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"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","sourcesContent":["// `scopebond-hook init` — scaffold the local enrollment: a machine signing key, a\n// gateway countersigning key and a starter policy. Keys and receipts stay on the\n// machine; no credentials are handled.\n\nimport { mkdirSync, writeFileSync, existsSync, readFileSync } from \"node:fs\";\nimport { dirname, join } from \"node:path\";\nimport { loadOrCreateAttester } from \"@scopebond/gateway/node\";\nimport { createSigner } from \"@scopebond/sdk\";\nimport { compile, defaultRules, loadRules, saveRules, rulesPath } from \"./rules.js\";\nimport { readHarnessConfig, projectHarnessFile, harnessEntryMatches, backupHarnessConfig } from \"./install.js\";\nimport { hookCommand } from \"./version.js\";\n\nexport function scaffold(dir: string, opts: { force?: boolean } = {}): { agentKid: string; policyPath: string; rulesPath: string } {\n mkdirSync(dir, { recursive: true });\n const keyPath = join(dir, \"agent.key\");\n const attesterPath = join(dir, \"attester.key\");\n const policyPath = join(dir, \"policy.json\");\n // Never let the signing keys, the Cloud credential or the local log be committed.\n const gitignorePath = join(dir, \".gitignore\");\n if (!existsSync(gitignorePath)) writeFileSync(gitignorePath, \"*\\n\");\n // Machine signing key (agent) + gateway countersigning key (attester). Reused if present.\n loadOrCreateAttester({ file: keyPath });\n loadOrCreateAttester({ file: attesterPath });\n const agent = createSigner({ privateKeyPem: readFileSync(keyPath, \"utf8\") });\n // The editable rule set beside the compiled policy, so \"edit the limits\" means editing a\n // readable list rather than a 700-character lookahead. `compile(defaultRules())` produces\n // the starter policy's exact patterns — `rules.test.mjs` pins that — so writing either\n // form enforces the same thing.\n const rulesFile = rulesPath(dir);\n if (!existsSync(rulesFile) || opts.force) saveRules(dir, defaultRules());\n if (!existsSync(policyPath) || opts.force) {\n const rules = loadRules(dir) ?? defaultRules();\n writeFileSync(policyPath, JSON.stringify(compile(rules, agent.kid), null, 2) + \"\\n\");\n }\n return { agentKid: agent.kid, policyPath, rulesPath: rulesFile };\n}\n\n/** Install the hook into the agent's own config file, merging with anything already\n * there (idempotent — safe to run twice). Returns the file it wrote. This is what\n * lets `connect` finish setup without the user hand-editing JSON.\n * `cwd` defaults to the current directory (the project root the user runs it in). */\nexport function installHarness(\n harness: \"claude\" | \"cursor\" | \"codex\",\n cwd: string = process.cwd(),\n /** The command to run per tool call. Defaults to the portable `npx` form; `init`\n * passes a pinned absolute path when it has one, which is ~7x faster to start. */\n command?: string,\n): string {\n const isRecord = (v: unknown): v is Record<string, unknown> => typeof v === \"object\" && v !== null && !Array.isArray(v);\n const file = projectHarnessFile(harness, cwd);\n const config = readHarnessConfig(file); // throws, leaving the file intact, if it is not a JSON object\n backupHarnessConfig(file); // keep the user's original beside it before the first change\n mkdirSync(dirname(file), { recursive: true });\n const hooks = isRecord(config.hooks) ? config.hooks : (config.hooks = {});\n const forHarness = (h: \"claude\" | \"cursor\" | \"codex\"): string =>\n command ? command.replace(/\\b(claude|cursor|codex)$/, h) : hookCommand(h);\n const cursorCmd = forHarness(\"cursor\");\n const claudeCmd = forHarness(\"claude\");\n const codexCmd = forHarness(\"codex\");\n // Match any existing Scopebond hook entry — npx, pinned absolute (either path\n // separator) or a legacy bare `scopebond-hook` — so re-running replaces the entry\n // instead of appending a second one. One shared matcher with `install`: a private\n // copy here missed the pinned Windows form and duplicated the hook.\n const entryMatches = harnessEntryMatches;\n if (harness === \"cursor\") {\n config.version = config.version ?? 1;\n for (const event of [\"beforeShellExecution\", \"beforeMCPExecution\", \"beforeReadFile\", \"afterFileEdit\"]) {\n const list = Array.isArray((hooks as Record<string, unknown>)[event]) ? (hooks as Record<string, unknown[]>)[event] : ((hooks as Record<string, unknown[]>)[event] = []);\n const existing = list.findIndex(entryMatches);\n if (existing >= 0) list[existing] = { command: cursorCmd }; else list.push({ command: cursorCmd });\n }\n } else {\n const list = Array.isArray((hooks as Record<string, unknown>).PreToolUse) ? (hooks as Record<string, unknown[]>).PreToolUse : ((hooks as Record<string, unknown[]>).PreToolUse = []);\n const existing = list.findIndex(entryMatches);\n // Codex matchers are regular expressions. No matcher means all supported tools.\n const entry = harness === \"codex\"\n ? { hooks: [{ type: \"command\", command: codexCmd, timeout: 30, statusMessage: \"Checking this action with Scopebond\" }] }\n : { matcher: \"*\", hooks: [{ type: \"command\", command: claudeCmd }] };\n if (existing >= 0) list[existing] = entry; else list.push(entry);\n }\n writeFileSync(file, JSON.stringify(config, null, 2) + \"\\n\");\n return file;\n}\n\n/** The harness configuration snippet to install the hook by hand. Defaults to the\n * version-pinned `npx` form; `init --no-install` passes the pinned path it chose, so\n * the printed snippet matches what `init` would have written. */\nexport function harnessSnippet(harness: \"claude\" | \"cursor\" | \"codex\", command?: string): string {\n const cmdFor = (h: \"claude\" | \"cursor\" | \"codex\"): string =>\n command ? command.replace(/\\b(claude|cursor|codex)$/, h) : hookCommand(h);\n if (harness === \"cursor\") {\n const cmd = cmdFor(\"cursor\");\n return JSON.stringify({\n version: 1,\n hooks: {\n beforeShellExecution: [{ command: cmd }],\n beforeMCPExecution: [{ command: cmd }],\n beforeReadFile: [{ command: cmd }],\n afterFileEdit: [{ command: cmd }],\n },\n }, null, 2);\n }\n if (harness === \"codex\") {\n return JSON.stringify({\n hooks: {\n PreToolUse: [{\n hooks: [{ type: \"command\", command: cmdFor(\"codex\"), timeout: 30, statusMessage: \"Checking this action with Scopebond\" }],\n }],\n },\n }, null, 2);\n }\n return JSON.stringify({\n hooks: {\n PreToolUse: [{ matcher: \"*\", hooks: [{ type: \"command\", command: cmdFor(\"claude\") }] }],\n },\n }, null, 2);\n}\n"]}
package/dist/install.d.ts CHANGED
@@ -4,6 +4,20 @@ export declare function userHome(): string;
4
4
  /** The user-level agent config file for a harness. `HOME`/`USERPROFILE` (via homedir)
5
5
  * roots it; SCOPEBOND_HOME does not move the agent's own config. */
6
6
  export declare function userHarnessFile(harness: Harness): string;
7
+ /** The project-level agent config file for a harness — what `init` writes, so the
8
+ * hook travels with the repository rather than the machine. */
9
+ export declare function projectHarnessFile(harness: Harness, cwd?: string): string;
10
+ /** Which config files actually carry a Scopebond hook for this harness. There are two
11
+ * places it can live — the project config `init` writes and the user config `install`
12
+ * writes — and `status`/`doctor` must look at both: checking only the user one told
13
+ * everyone who ran `init` that their install had failed. */
14
+ export interface HarnessScopes {
15
+ project: string | null;
16
+ user: string | null;
17
+ }
18
+ export declare function harnessScopes(harness: Harness, cwd?: string): HarnessScopes;
19
+ /** One word for where a harness is wired, for a status line. */
20
+ export declare function harnessScopeLabel(scopes: HarnessScopes): string;
7
21
  /** The file in the user home that pins trusted project policies: absolute project
8
22
  * config dir → SHA-256 of its policy.json. It lives under the protected home, so the
9
23
  * governed agent cannot write it. */
@@ -22,10 +36,23 @@ export declare function isTrustedProject(dir: string): boolean;
22
36
  export declare function resolveConfigDir(payloadCwd: string | undefined): string;
23
37
  /** A project policy present but ignored because it is not trusted (for status/doctor). */
24
38
  export declare function untrustedProjectPolicy(payloadCwd: string | undefined): string | null;
39
+ export declare const isScopebondHookCommand: (cmd: unknown) => boolean;
40
+ /** Whether a harness hook entry (either shape: a bare command, or a group with a
41
+ * nested `hooks` array) is a Scopebond entry. */
42
+ export declare const harnessEntryMatches: (e: unknown) => boolean;
25
43
  /** Read an agent's JSON config for merging. A missing file is an empty config; a file
26
44
  * that exists but does not parse as a JSON object is an error — rewriting it would
27
45
  * silently delete the user's other settings, so the installer stops instead. */
28
46
  export declare function readHarnessConfig(file: string): Record<string, unknown>;
47
+ /** Copy an agent config aside before changing it, once. Re-running an installer must not
48
+ * overwrite the backup with the already-modified file, or the original is lost on the
49
+ * second run — so an existing backup is left alone. Returns the backup path when one was
50
+ * written.
51
+ *
52
+ * This exists because these installers rewrite files the user did not create and may not
53
+ * know are involved (`~/.claude/settings.json` holds their theme, plugins and
54
+ * permissions), and the undo was previously "hope the merge was right". */
55
+ export declare function backupHarnessConfig(file: string): string | null;
29
56
  /** Write (idempotently) a harness hook entry pointing at an explicit command string.
30
57
  * Shared by the project installer (npx command) and the user installer (absolute path). */
31
58
  export declare function writeHarnessConfig(file: string, harness: Harness, command: string): string;
@@ -36,10 +63,26 @@ export declare function removeHarnessConfig(file: string): boolean;
36
63
  export declare function cursorDetected(): boolean;
37
64
  /** Detect a Codex install by its user config directory. */
38
65
  export declare function codexDetected(): boolean;
39
- /** The absolute command a user-level harness entry runs: the current Node executable
40
- * and the absolute path to this CLI, so it does not depend on PATH. */
66
+ /** The absolute command a harness entry runs: the current Node executable and the
67
+ * absolute path to this CLI, so it does not depend on PATH.
68
+ *
69
+ * Both paths are always quoted on Windows, even without a space in them. A harness
70
+ * runs this string through a shell, and that shell may be bash (Git Bash, WSL, a
71
+ * dev container), where an unquoted Windows path loses every backslash and the
72
+ * hook dies with MODULE_NOT_FOUND. Inside double quotes bash leaves a backslash
73
+ * alone unless it precedes $ ` " \ or a newline — none of which occur in a Windows
74
+ * path — and cmd.exe and PowerShell accept quoted paths too. */
41
75
  export declare function absoluteHookCommand(cliPath: string, harness: Harness): string;
42
76
  export declare function isHarnessConfigured(file: string): boolean;
77
+ /** Every Scopebond command string a harness config currently runs. `doctor` uses it to
78
+ * check that a pinned command still resolves: a hook entry that cannot start is worse
79
+ * than none, because a harness can read the failure as "no hook". */
80
+ export declare function configuredHookCommands(file: string): string[];
81
+ /** Whether a configured hook command can actually start. An `npx` form always can
82
+ * (npm resolves it); a pinned command names two absolute paths — the Node binary and
83
+ * the CLI — and either can go away: a Node version manager switching versions, or the
84
+ * Scopebond home being cleared. Both are exactly what this check exists to catch. */
85
+ export declare function hookCommandResolves(command: string): boolean;
43
86
  /** Remove the user-level home (keys, policy, receipts). */
44
87
  export declare function purgeHome(): void;
45
88
  //# sourceMappingURL=install.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../src/install.ts"],"names":[],"mappings":"AAaA,MAAM,MAAM,OAAO,GAAG,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEpD,gFAAgF;AAChF,wBAAgB,QAAQ,IAAI,MAAM,CAEjC;AAED;qEACqE;AACrE,wBAAgB,eAAe,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAIxD;AAED;;sCAEsC;AACtC,wBAAgB,mBAAmB,IAAI,MAAM,CAE5C;AAYD;uFACuF;AACvF,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAQtD;AAED,wEAAwE;AACxE,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAGrD;AAED;;;;;iFAKiF;AACjF,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAavE;AAED,0FAA0F;AAC1F,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAIpF;AAaD;;iFAEiF;AACjF,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAUvE;AAED;4FAC4F;AAC5F,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAwB1F;AAED;gDACgD;AAChD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAUzD;AAED,4DAA4D;AAC5D,wBAAgB,cAAc,IAAI,OAAO,CAExC;AAED,2DAA2D;AAC3D,wBAAgB,aAAa,IAAI,OAAO,CAEvC;AAED;wEACwE;AACxE,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,MAAM,CAG7E;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAOzD;AAED,2DAA2D;AAC3D,wBAAgB,SAAS,IAAI,IAAI,CAEhC"}
1
+ {"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../src/install.ts"],"names":[],"mappings":"AAaA,MAAM,MAAM,OAAO,GAAG,QAAQ,GAAG,QAAQ,GAAG,OAAO,CAAC;AAEpD,gFAAgF;AAChF,wBAAgB,QAAQ,IAAI,MAAM,CAEjC;AAED;qEACqE;AACrE,wBAAgB,eAAe,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,CAIxD;AAED;gEACgE;AAChE,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,GAAE,MAAsB,GAAG,MAAM,CAIxF;AAED;;;6DAG6D;AAC7D,MAAM,WAAW,aAAa;IAAG,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAAA;CAAE;AAE9E,wBAAgB,aAAa,CAAC,OAAO,EAAE,OAAO,EAAE,GAAG,GAAE,MAAsB,GAAG,aAAa,CAO1F;AAED,gEAAgE;AAChE,wBAAgB,iBAAiB,CAAC,MAAM,EAAE,aAAa,GAAG,MAAM,CAK/D;AAED;;sCAEsC;AACtC,wBAAgB,mBAAmB,IAAI,MAAM,CAE5C;AAYD;uFACuF;AACvF,wBAAgB,kBAAkB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAQtD;AAED,wEAAwE;AACxE,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAGrD;AAED;;;;;iFAKiF;AACjF,wBAAgB,gBAAgB,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,CAavE;AAED,0FAA0F;AAC1F,wBAAgB,sBAAsB,CAAC,UAAU,EAAE,MAAM,GAAG,SAAS,GAAG,MAAM,GAAG,IAAI,CAIpF;AAyBD,eAAO,MAAM,sBAAsB,GAAI,KAAK,OAAO,KAAG,OAgBrD,CAAC;AAGF;kDACkD;AAClD,eAAO,MAAM,mBAAmB,GAAI,GAAG,OAAO,KAAG,OACkF,CAAC;AAGpI;;iFAEiF;AACjF,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAUvE;AAED;;;;;;;4EAO4E;AAC5E,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAK/D;AAED;4FAC4F;AAC5F,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM,CAyB1F;AAED;gDACgD;AAChD,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAUzD;AAED,4DAA4D;AAC5D,wBAAgB,cAAc,IAAI,OAAO,CAExC;AAED,2DAA2D;AAC3D,wBAAgB,aAAa,IAAI,OAAO,CAEvC;AAED;;;;;;;;iEAQiE;AACjE,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,OAAO,GAAG,MAAM,CAG7E;AAED,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAOzD;AAED;;sEAEsE;AACtE,wBAAgB,sBAAsB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAc7D;AAED;;;sFAGsF;AACtF,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,OAAO,CAa5D;AAED,2DAA2D;AAC3D,wBAAgB,SAAS,IAAI,IAAI,CAEhC"}
package/dist/install.js CHANGED
@@ -6,7 +6,7 @@
6
6
  // trusted that exact policy (`scopebond trust`, or `init` in the project): a cloned
7
7
  // repository, or an agent inside it, must not be able to swap in its own policy.
8
8
  import { createHash } from "node:crypto";
9
- import { mkdirSync, writeFileSync, existsSync, readFileSync, rmSync } from "node:fs";
9
+ import { mkdirSync, writeFileSync, existsSync, readFileSync, rmSync, copyFileSync } from "node:fs";
10
10
  import { homedir } from "node:os";
11
11
  import { dirname, join, resolve } from "node:path";
12
12
  /** The user-level Scopebond home. `SCOPEBOND_HOME` overrides it (tests, CI). */
@@ -22,6 +22,33 @@ export function userHarnessFile(harness) {
22
22
  return join(homedir(), ".codex", "hooks.json");
23
23
  return join(homedir(), ".claude", "settings.json");
24
24
  }
25
+ /** The project-level agent config file for a harness — what `init` writes, so the
26
+ * hook travels with the repository rather than the machine. */
27
+ export function projectHarnessFile(harness, cwd = process.cwd()) {
28
+ if (harness === "cursor")
29
+ return join(cwd, ".cursor", "hooks.json");
30
+ if (harness === "codex")
31
+ return join(cwd, ".codex", "hooks.json");
32
+ return join(cwd, ".claude", "settings.json");
33
+ }
34
+ export function harnessScopes(harness, cwd = process.cwd()) {
35
+ const project = projectHarnessFile(harness, cwd);
36
+ const user = userHarnessFile(harness);
37
+ return {
38
+ project: isHarnessConfigured(project) ? project : null,
39
+ user: isHarnessConfigured(user) ? user : null,
40
+ };
41
+ }
42
+ /** One word for where a harness is wired, for a status line. */
43
+ export function harnessScopeLabel(scopes) {
44
+ if (scopes.project && scopes.user)
45
+ return "configured (project + user)";
46
+ if (scopes.project)
47
+ return "configured (this project)";
48
+ if (scopes.user)
49
+ return "configured (user-level)";
50
+ return "";
51
+ }
25
52
  /** The file in the user home that pins trusted project policies: absolute project
26
53
  * config dir → SHA-256 of its policy.json. It lives under the protected home, so the
27
54
  * governed agent cannot write it. */
@@ -96,11 +123,52 @@ export function untrustedProjectPolicy(payloadCwd) {
96
123
  return dirs.find((d) => existsSync(join(d, "policy.json")) && !isTrustedProject(d)) ?? null;
97
124
  }
98
125
  const isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
99
- const isScopebond = (cmd) => typeof cmd === "string" && (/@scopebond\/hook|scopebond-hook(\s|$)/.test(cmd) ||
100
- // the user-level absolute form: `<node> <…/cli.js> claude|cursor|codex`
101
- /cli\.js["']?\s+(claude|cursor|codex)\s*$/.test(cmd) ||
102
- /(^|["\s])scopebond(["'\s]).*\b(claude|cursor|codex)\b/.test(cmd));
103
- const entryMatches = (e) => isRecord(e) && (isScopebond(e.command) || (Array.isArray(e.hooks) && e.hooks.some((h) => isRecord(h) && isScopebond(h.command))));
126
+ /** Whether a harness command string is one of ours. This is the single matcher for
127
+ * every form the installer has ever written, and it is what makes re-running `init`
128
+ * or `install` replace the entry instead of appending a second one:
129
+ * - `npx -y @scopebond/hook@0.6.0 claude` (portable)
130
+ * - `"<node>" "<…/@scopebond/hook/dist/cli.js>" claude` (pinned, POSIX)
131
+ * - `"<node>" "<…\@scopebond\hook\dist\cli.js>" claude` (pinned, Windows)
132
+ * - `scopebond-hook claude` (legacy global binary)
133
+ * The separator class matters: a Windows pinned path spells the scope
134
+ * `@scopebond\hook`, and matching only `@scopebond/hook` let a second `init`
135
+ * install a duplicate hook — which would double-check every tool call.
136
+ *
137
+ * Written with substring and token tests rather than one regular expression: the
138
+ * previous `(^|["\s])scopebond(["'\s]).*\b(claude|cursor|codex)\b` backtracked
139
+ * polynomially on a long run of `\tscopebond\t` (CodeQL js/polynomial-redos). The
140
+ * input is a command out of a config file rather than a request, so this was not
141
+ * reachable by an attacker — but a linear check is also the clearer one. */
142
+ const HARNESS_SUBCOMMANDS = new Set(["claude", "cursor", "codex"]);
143
+ /** Long enough for any real command; a bound keeps the work linear and small. */
144
+ const MAX_COMMAND = 4096;
145
+ const unquote = (token) => token.replace(/^["']+|["']+$/g, "");
146
+ export const isScopebondHookCommand = (cmd) => {
147
+ if (typeof cmd !== "string" || cmd.length === 0 || cmd.length > MAX_COMMAND)
148
+ return false;
149
+ const lower = cmd.toLowerCase();
150
+ // The package itself, in either path spelling (`@scopebond/hook`, `@scopebond\hook`).
151
+ if (lower.includes("@scopebond/hook") || lower.includes("@scopebond\\hook"))
152
+ return true;
153
+ // The legacy global binary, as a whole token.
154
+ if (/(^|[\s"'])scopebond-hook([\s"']|$)/.test(cmd))
155
+ return true;
156
+ // An absolute or bare form whose last argument is one of our harness subcommands:
157
+ // `<node> <…/cli.js> claude`, `scopebond cursor`.
158
+ const tokens = cmd.split(/\s+/).filter(Boolean);
159
+ const last = unquote(tokens[tokens.length - 1] ?? "");
160
+ if (!HARNESS_SUBCOMMANDS.has(last))
161
+ return false;
162
+ return tokens.some((token) => {
163
+ const t = unquote(token).toLowerCase();
164
+ return t.includes("scopebond") || /cli\.(m|c)?js$/.test(t);
165
+ });
166
+ };
167
+ const isScopebond = isScopebondHookCommand;
168
+ /** Whether a harness hook entry (either shape: a bare command, or a group with a
169
+ * nested `hooks` array) is a Scopebond entry. */
170
+ export const harnessEntryMatches = (e) => isRecord(e) && (isScopebond(e.command) || (Array.isArray(e.hooks) && e.hooks.some((h) => isRecord(h) && isScopebond(h.command))));
171
+ const entryMatches = harnessEntryMatches;
104
172
  /** Read an agent's JSON config for merging. A missing file is an empty config; a file
105
173
  * that exists but does not parse as a JSON object is an error — rewriting it would
106
174
  * silently delete the user's other settings, so the installer stops instead. */
@@ -121,10 +189,33 @@ export function readHarnessConfig(file) {
121
189
  throw new Error(`${file} is not a JSON object; fix or move it, then run this again — it was left unchanged`);
122
190
  return parsed;
123
191
  }
192
+ /** Copy an agent config aside before changing it, once. Re-running an installer must not
193
+ * overwrite the backup with the already-modified file, or the original is lost on the
194
+ * second run — so an existing backup is left alone. Returns the backup path when one was
195
+ * written.
196
+ *
197
+ * This exists because these installers rewrite files the user did not create and may not
198
+ * know are involved (`~/.claude/settings.json` holds their theme, plugins and
199
+ * permissions), and the undo was previously "hope the merge was right". */
200
+ export function backupHarnessConfig(file) {
201
+ if (!existsSync(file))
202
+ return null;
203
+ const backup = `${file}.scopebond-backup`;
204
+ if (existsSync(backup))
205
+ return null;
206
+ try {
207
+ copyFileSync(file, backup);
208
+ return backup;
209
+ }
210
+ catch {
211
+ return null;
212
+ }
213
+ }
124
214
  /** Write (idempotently) a harness hook entry pointing at an explicit command string.
125
215
  * Shared by the project installer (npx command) and the user installer (absolute path). */
126
216
  export function writeHarnessConfig(file, harness, command) {
127
217
  const config = readHarnessConfig(file);
218
+ backupHarnessConfig(file);
128
219
  mkdirSync(dirname(file), { recursive: true });
129
220
  const hooks = isRecord(config.hooks) ? config.hooks : (config.hooks = {});
130
221
  if (harness === "cursor") {
@@ -183,10 +274,17 @@ export function cursorDetected() {
183
274
  export function codexDetected() {
184
275
  return existsSync(join(homedir(), ".codex"));
185
276
  }
186
- /** The absolute command a user-level harness entry runs: the current Node executable
187
- * and the absolute path to this CLI, so it does not depend on PATH. */
277
+ /** The absolute command a harness entry runs: the current Node executable and the
278
+ * absolute path to this CLI, so it does not depend on PATH.
279
+ *
280
+ * Both paths are always quoted on Windows, even without a space in them. A harness
281
+ * runs this string through a shell, and that shell may be bash (Git Bash, WSL, a
282
+ * dev container), where an unquoted Windows path loses every backslash and the
283
+ * hook dies with MODULE_NOT_FOUND. Inside double quotes bash leaves a backslash
284
+ * alone unless it precedes $ ` " \ or a newline — none of which occur in a Windows
285
+ * path — and cmd.exe and PowerShell accept quoted paths too. */
188
286
  export function absoluteHookCommand(cliPath, harness) {
189
- const quote = (s) => (/\s/.test(s) ? `"${s}"` : s);
287
+ const quote = (s) => (process.platform === "win32" || /\s/.test(s) ? `"${s}"` : s);
190
288
  return `${quote(process.execPath)} ${quote(cliPath)} ${harness}`;
191
289
  }
192
290
  export function isHarnessConfigured(file) {
@@ -202,6 +300,57 @@ export function isHarnessConfigured(file) {
202
300
  return false;
203
301
  }
204
302
  }
303
+ /** Every Scopebond command string a harness config currently runs. `doctor` uses it to
304
+ * check that a pinned command still resolves: a hook entry that cannot start is worse
305
+ * than none, because a harness can read the failure as "no hook". */
306
+ export function configuredHookCommands(file) {
307
+ if (!existsSync(file))
308
+ return [];
309
+ const found = [];
310
+ const collect = (entry) => {
311
+ if (!isRecord(entry))
312
+ return;
313
+ if (isScopebond(entry.command))
314
+ found.push(String(entry.command));
315
+ if (Array.isArray(entry.hooks))
316
+ for (const h of entry.hooks)
317
+ collect(h);
318
+ };
319
+ try {
320
+ const p = JSON.parse(readFileSync(file, "utf8"));
321
+ if (!isRecord(p) || !isRecord(p.hooks))
322
+ return [];
323
+ for (const list of Object.values(p.hooks))
324
+ if (Array.isArray(list))
325
+ for (const e of list)
326
+ collect(e);
327
+ }
328
+ catch {
329
+ return [];
330
+ }
331
+ return found;
332
+ }
333
+ /** Whether a configured hook command can actually start. An `npx` form always can
334
+ * (npm resolves it); a pinned command names two absolute paths — the Node binary and
335
+ * the CLI — and either can go away: a Node version manager switching versions, or the
336
+ * Scopebond home being cleared. Both are exactly what this check exists to catch. */
337
+ export function hookCommandResolves(command) {
338
+ if (/(^|\s)npx(\.cmd)?\s/.test(command))
339
+ return true;
340
+ // `"<node>" "<cli.js>" claude`: check every token that names a file on disk.
341
+ const tokens = command.match(/"[^"]+"|\S+/g) ?? [];
342
+ let checked = 0;
343
+ for (const raw of tokens) {
344
+ const candidate = raw.replace(/^"|"$/g, "");
345
+ if (!/\.(m|c)?js$|\.exe$/i.test(candidate))
346
+ continue;
347
+ checked += 1;
348
+ if (!existsSync(candidate))
349
+ return false;
350
+ }
351
+ // A bare `node cli.js` form on POSIX has no .exe to check; the .js still counted.
352
+ return checked > 0 || !/[\\/]/.test(command.split(/\s+/)[0] ?? "");
353
+ }
205
354
  /** Remove the user-level home (keys, policy, receipts). */
206
355
  export function purgeHome() {
207
356
  rmSync(userHome(), { recursive: true, force: true });