@scopebond/hook 0.7.0 → 0.8.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -4,10 +4,10 @@
4
4
  export { mapClaudeToolUse, mapCodexToolUse, mapCursorEvent, fillPushBranch } from "./map.js";
5
5
  export { createHookRuntime, starterPolicy, upgradeStarterPolicy } from "./runtime.js";
6
6
  export { compile, defaultRules, describeRules, loadRules, saveRules, rulesPath, pathRuleFor, RULES_FILE, } from "./rules.js";
7
- export { scaffold, harnessSnippet, installHarness } from "./init.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";
7
+ export { scaffold, harnessSnippet, installHarness, placeHook } from "./init.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, localHarnessFile, isMachineSpecificCommand, gitShareState, excludeFromGit, pruneHarnessEntries, } from "./install.js";
9
9
  export { explainDeny, describeAction, findClause } from "./explain.js";
10
10
  export { ensureDurableRuntime, isEphemeralPath, nodeModulesRootOf, runtimeRoot, runtimeDirFor, pinnedCliPath, } from "./runtime-install.js";
11
11
  export { connectCloud, loadConnection, attachExporter, flushBounded, connectionPath } from "./cloud.js";
12
- export { scrubSecrets, scrubParam, redactCommand, digest, sha256 } from "./minimize.js";
12
+ export { scrubSecrets, scrubParam, redactCommand, digest, sha256, keyedDigest, useDigestKey, loadOrCreateDigestKey } from "./minimize.js";
13
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,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"]}
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,SAAS,EAAE,MAAM,WAAW,CAAC;AAEhF,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,EAC3C,gBAAgB,EAAE,wBAAwB,EAAE,aAAa,EAAE,cAAc,EAAE,mBAAmB,GAC/F,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,WAAW,EAAE,YAAY,EAAE,qBAAqB,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, placeHook } from \"./init.js\";\nexport type { HookPlacement } 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 localHarnessFile, isMachineSpecificCommand, gitShareState, excludeFromGit, pruneHarnessEntries,\n} from \"./install.js\";\nexport type { Harness, HarnessScopes, GitShare } 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, keyedDigest, useDigestKey, loadOrCreateDigestKey } from \"./minimize.js\";\n"]}
package/dist/init.d.ts CHANGED
@@ -5,14 +5,45 @@ export declare function scaffold(dir: string, opts?: {
5
5
  policyPath: string;
6
6
  rulesPath: string;
7
7
  };
8
+ /** Where `init` put the hook, and why — for the lines it prints. */
9
+ export interface HookPlacement {
10
+ /** The config file written. */
11
+ file: string;
12
+ /** The command written into it. */
13
+ command: string;
14
+ /** `shared`: the project config a team commits (portable command only).
15
+ * `personal`: a file only this clone uses (the pinned, machine-specific command). */
16
+ scope: "shared" | "personal";
17
+ /** One line for the user when the placement differs from what they might expect. */
18
+ note?: string;
19
+ /** Machine-specific entries moved out of a shared config file (a repair). */
20
+ repaired: number;
21
+ }
22
+ /** Place the hook for a project so that nothing machine-specific reaches a file git
23
+ * shares. A pinned command (`"<node>" "<…/cli.js>" claude`) is fast but names paths that
24
+ * exist only here; in a committed `.claude/settings.json` it cannot start on a
25
+ * teammate's machine, and an agent treats a hook that cannot start as a non-blocking
26
+ * error — so the agent there would run unchecked, while the file says it is governed.
27
+ *
28
+ * - `shared` (or no pinned command): the portable `npx` form in the project config.
29
+ * - Claude Code, pinned: `.claude/settings.local.json`, kept out of git for this clone;
30
+ * any machine-specific entry an older `init` wrote into `settings.json` is removed.
31
+ * - Cursor / Codex, pinned: they have no personal file, so the project file is used
32
+ * only if git does not already track it (then excluded for this clone); a tracked
33
+ * file gets the portable form. */
34
+ export declare function placeHook(harness: "claude" | "cursor" | "codex", cwd: string, pinned: string | undefined, opts?: {
35
+ shared?: boolean;
36
+ }): HookPlacement;
8
37
  /** Install the hook into the agent's own config file, merging with anything already
9
38
  * there (idempotent — safe to run twice). Returns the file it wrote. This is what
10
39
  * lets `connect` finish setup without the user hand-editing JSON.
11
- * `cwd` defaults to the current directory (the project root the user runs it in). */
40
+ * `cwd` defaults to the current directory (the project root the user runs it in).
41
+ * Writes exactly where it is told; `placeHook` decides where a command may go. */
12
42
  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;
43
+ /** The command to run per tool call. Defaults to the portable `npx` form. */
44
+ command?: string,
45
+ /** The file to write; defaults to the shared project config. */
46
+ target?: string): string;
16
47
  /** The harness configuration snippet to install the hook by hand. Defaults to the
17
48
  * version-pinned `npx` form; `init --no-install` passes the pinned path it chose, so
18
49
  * the printed snippet matches what `init` would have written. */
@@ -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,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"}
1
+ {"version":3,"file":"init.d.ts","sourceRoot":"","sources":["../src/init.ts"],"names":[],"mappings":"AAgBA,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,CAyBjI;AAED,oEAAoE;AACpE,MAAM,WAAW,aAAa;IAC5B,+BAA+B;IAC/B,IAAI,EAAE,MAAM,CAAC;IACb,mCAAmC;IACnC,OAAO,EAAE,MAAM,CAAC;IAChB;0FACsF;IACtF,KAAK,EAAE,QAAQ,GAAG,UAAU,CAAC;IAC7B,oFAAoF;IACpF,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,6EAA6E;IAC7E,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED;;;;;;;;;;;qCAWqC;AACrC,wBAAgB,SAAS,CACvB,OAAO,EAAE,QAAQ,GAAG,QAAQ,GAAG,OAAO,EACtC,GAAG,EAAE,MAAM,EACX,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,IAAI,GAAE;IAAE,MAAM,CAAC,EAAE,OAAO,CAAA;CAAO,GAC9B,aAAa,CAiDf;AAED;;;;mFAImF;AACnF,wBAAgB,cAAc,CAC5B,OAAO,EAAE,QAAQ,GAAG,QAAQ,GAAG,OAAO,EACtC,GAAG,GAAE,MAAsB;AAC3B,6EAA6E;AAC7E,OAAO,CAAC,EAAE,MAAM;AAChB,gEAAgE;AAChE,MAAM,CAAC,EAAE,MAAM,GACd,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
@@ -6,8 +6,9 @@ import { dirname, join } from "node:path";
6
6
  import { loadOrCreateAttester } from "@scopebond/gateway/node";
7
7
  import { createSigner } from "@scopebond/sdk";
8
8
  import { compile, defaultRules, loadRules, saveRules, rulesPath } from "./rules.js";
9
- import { readHarnessConfig, projectHarnessFile, harnessEntryMatches, backupHarnessConfig } from "./install.js";
9
+ import { readHarnessConfig, projectHarnessFile, localHarnessFile, harnessEntryMatches, backupHarnessConfig, gitShareState, excludeFromGit, pruneHarnessEntries, configuredHookCommands, isMachineSpecificCommand, } from "./install.js";
10
10
  import { hookCommand } from "./version.js";
11
+ import { loadOrCreateDigestKey } from "./minimize.js";
11
12
  export function scaffold(dir, opts = {}) {
12
13
  mkdirSync(dir, { recursive: true });
13
14
  const keyPath = join(dir, "agent.key");
@@ -20,6 +21,8 @@ export function scaffold(dir, opts = {}) {
20
21
  // Machine signing key (agent) + gateway countersigning key (attester). Reused if present.
21
22
  loadOrCreateAttester({ file: keyPath });
22
23
  loadOrCreateAttester({ file: attesterPath });
24
+ // The per-machine key for keyed command and argument digests (see minimize.ts).
25
+ loadOrCreateDigestKey(dir);
23
26
  const agent = createSigner({ privateKeyPem: readFileSync(keyPath, "utf8") });
24
27
  // The editable rule set beside the compiled policy, so "edit the limits" means editing a
25
28
  // readable list rather than a 700-character lookahead. `compile(defaultRules())` produces
@@ -34,16 +37,81 @@ export function scaffold(dir, opts = {}) {
34
37
  }
35
38
  return { agentKid: agent.kid, policyPath, rulesPath: rulesFile };
36
39
  }
40
+ /** Place the hook for a project so that nothing machine-specific reaches a file git
41
+ * shares. A pinned command (`"<node>" "<…/cli.js>" claude`) is fast but names paths that
42
+ * exist only here; in a committed `.claude/settings.json` it cannot start on a
43
+ * teammate's machine, and an agent treats a hook that cannot start as a non-blocking
44
+ * error — so the agent there would run unchecked, while the file says it is governed.
45
+ *
46
+ * - `shared` (or no pinned command): the portable `npx` form in the project config.
47
+ * - Claude Code, pinned: `.claude/settings.local.json`, kept out of git for this clone;
48
+ * any machine-specific entry an older `init` wrote into `settings.json` is removed.
49
+ * - Cursor / Codex, pinned: they have no personal file, so the project file is used
50
+ * only if git does not already track it (then excluded for this clone); a tracked
51
+ * file gets the portable form. */
52
+ export function placeHook(harness, cwd, pinned, opts = {}) {
53
+ const shared = projectHarnessFile(harness, cwd);
54
+ const portable = hookCommand(harness);
55
+ const local = localHarnessFile(harness, cwd);
56
+ if (opts.shared || !pinned) {
57
+ const file = installHarness(harness, cwd, portable);
58
+ // One hook per project: a personal copy beside a shared one would check every action twice.
59
+ if (local)
60
+ pruneHarnessEntries(local, () => true);
61
+ return { file, command: portable, scope: "shared", repaired: 0 };
62
+ }
63
+ if (local) {
64
+ const state = gitShareState(local);
65
+ if (state === "tracked") {
66
+ const file = installHarness(harness, cwd, portable);
67
+ return { file, command: portable, scope: "shared", repaired: 0,
68
+ note: `${local} is committed to git, so the portable command was used (it starts on any machine)` };
69
+ }
70
+ // An existing portable hook in the shared file is the team's; leave it rather than
71
+ // running two hooks, and say how to get the fast one.
72
+ if (configuredHookCommands(shared).some((c) => !isMachineSpecificCommand(c))) {
73
+ return { file: shared, command: portable, scope: "shared", repaired: 0,
74
+ note: `kept the shared hook already in ${shared}; remove it there to pin a faster one on this machine` };
75
+ }
76
+ // A personal file git would still add is not personal: if it cannot be excluded,
77
+ // fall back to the portable command rather than leave a pinned path to be committed.
78
+ if (state === "untracked" && !excludeFromGit(local)) {
79
+ const file = installHarness(harness, cwd, portable);
80
+ pruneHarnessEntries(local, () => true);
81
+ pruneHarnessEntries(shared, isMachineSpecificCommand);
82
+ return { file, command: portable, scope: "shared", repaired: 0,
83
+ note: `${local} could not be kept out of git, so the portable command was used` };
84
+ }
85
+ const file = installHarness(harness, cwd, pinned, local);
86
+ const repaired = pruneHarnessEntries(shared, isMachineSpecificCommand);
87
+ return { file, command: pinned, scope: "personal", repaired };
88
+ }
89
+ const state = gitShareState(shared);
90
+ if (state === "tracked") {
91
+ const file = installHarness(harness, cwd, portable);
92
+ return { file, command: portable, scope: "shared", repaired: 0,
93
+ note: `${shared} is committed to git, so the portable command was used (it starts on any machine)` };
94
+ }
95
+ if (state === "untracked" && !excludeFromGit(shared)) {
96
+ const file = installHarness(harness, cwd, portable);
97
+ return { file, command: portable, scope: "shared", repaired: 0,
98
+ note: `${shared} could not be kept out of git, so the portable command was used` };
99
+ }
100
+ const file = installHarness(harness, cwd, pinned);
101
+ return { file, command: pinned, scope: "personal", repaired: 0 };
102
+ }
37
103
  /** Install the hook into the agent's own config file, merging with anything already
38
104
  * there (idempotent — safe to run twice). Returns the file it wrote. This is what
39
105
  * lets `connect` finish setup without the user hand-editing JSON.
40
- * `cwd` defaults to the current directory (the project root the user runs it in). */
106
+ * `cwd` defaults to the current directory (the project root the user runs it in).
107
+ * Writes exactly where it is told; `placeHook` decides where a command may go. */
41
108
  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) {
109
+ /** The command to run per tool call. Defaults to the portable `npx` form. */
110
+ command,
111
+ /** The file to write; defaults to the shared project config. */
112
+ target) {
45
113
  const isRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
46
- const file = projectHarnessFile(harness, cwd);
114
+ const file = target ?? projectHarnessFile(harness, cwd);
47
115
  const config = readHarnessConfig(file); // throws, leaving the file intact, if it is not a JSON object
48
116
  backupHarnessConfig(file); // keep the user's original beside it before the first change
49
117
  mkdirSync(dirname(file), { recursive: true });
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 { 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"]}
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 {\n readHarnessConfig, projectHarnessFile, localHarnessFile, harnessEntryMatches, backupHarnessConfig,\n gitShareState, excludeFromGit, pruneHarnessEntries, configuredHookCommands, isMachineSpecificCommand,\n} from \"./install.js\";\nimport { hookCommand } from \"./version.js\";\nimport { loadOrCreateDigestKey } from \"./minimize.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 // The per-machine key for keyed command and argument digests (see minimize.ts).\n loadOrCreateDigestKey(dir);\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/** Where `init` put the hook, and why — for the lines it prints. */\nexport interface HookPlacement {\n /** The config file written. */\n file: string;\n /** The command written into it. */\n command: string;\n /** `shared`: the project config a team commits (portable command only).\n * `personal`: a file only this clone uses (the pinned, machine-specific command). */\n scope: \"shared\" | \"personal\";\n /** One line for the user when the placement differs from what they might expect. */\n note?: string;\n /** Machine-specific entries moved out of a shared config file (a repair). */\n repaired: number;\n}\n\n/** Place the hook for a project so that nothing machine-specific reaches a file git\n * shares. A pinned command (`\"<node>\" \"<…/cli.js>\" claude`) is fast but names paths that\n * exist only here; in a committed `.claude/settings.json` it cannot start on a\n * teammate's machine, and an agent treats a hook that cannot start as a non-blocking\n * error — so the agent there would run unchecked, while the file says it is governed.\n *\n * - `shared` (or no pinned command): the portable `npx` form in the project config.\n * - Claude Code, pinned: `.claude/settings.local.json`, kept out of git for this clone;\n * any machine-specific entry an older `init` wrote into `settings.json` is removed.\n * - Cursor / Codex, pinned: they have no personal file, so the project file is used\n * only if git does not already track it (then excluded for this clone); a tracked\n * file gets the portable form. */\nexport function placeHook(\n harness: \"claude\" | \"cursor\" | \"codex\",\n cwd: string,\n pinned: string | undefined,\n opts: { shared?: boolean } = {},\n): HookPlacement {\n const shared = projectHarnessFile(harness, cwd);\n const portable = hookCommand(harness);\n const local = localHarnessFile(harness, cwd);\n if (opts.shared || !pinned) {\n const file = installHarness(harness, cwd, portable);\n // One hook per project: a personal copy beside a shared one would check every action twice.\n if (local) pruneHarnessEntries(local, () => true);\n return { file, command: portable, scope: \"shared\", repaired: 0 };\n }\n if (local) {\n const state = gitShareState(local);\n if (state === \"tracked\") {\n const file = installHarness(harness, cwd, portable);\n return { file, command: portable, scope: \"shared\", repaired: 0,\n note: `${local} is committed to git, so the portable command was used (it starts on any machine)` };\n }\n // An existing portable hook in the shared file is the team's; leave it rather than\n // running two hooks, and say how to get the fast one.\n if (configuredHookCommands(shared).some((c) => !isMachineSpecificCommand(c))) {\n return { file: shared, command: portable, scope: \"shared\", repaired: 0,\n note: `kept the shared hook already in ${shared}; remove it there to pin a faster one on this machine` };\n }\n // A personal file git would still add is not personal: if it cannot be excluded,\n // fall back to the portable command rather than leave a pinned path to be committed.\n if (state === \"untracked\" && !excludeFromGit(local)) {\n const file = installHarness(harness, cwd, portable);\n pruneHarnessEntries(local, () => true);\n pruneHarnessEntries(shared, isMachineSpecificCommand);\n return { file, command: portable, scope: \"shared\", repaired: 0,\n note: `${local} could not be kept out of git, so the portable command was used` };\n }\n const file = installHarness(harness, cwd, pinned, local);\n const repaired = pruneHarnessEntries(shared, isMachineSpecificCommand);\n return { file, command: pinned, scope: \"personal\", repaired };\n }\n const state = gitShareState(shared);\n if (state === \"tracked\") {\n const file = installHarness(harness, cwd, portable);\n return { file, command: portable, scope: \"shared\", repaired: 0,\n note: `${shared} is committed to git, so the portable command was used (it starts on any machine)` };\n }\n if (state === \"untracked\" && !excludeFromGit(shared)) {\n const file = installHarness(harness, cwd, portable);\n return { file, command: portable, scope: \"shared\", repaired: 0,\n note: `${shared} could not be kept out of git, so the portable command was used` };\n }\n const file = installHarness(harness, cwd, pinned);\n return { file, command: pinned, scope: \"personal\", repaired: 0 };\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).\n * Writes exactly where it is told; `placeHook` decides where a command may go. */\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. */\n command?: string,\n /** The file to write; defaults to the shared project config. */\n target?: string,\n): string {\n const isRecord = (v: unknown): v is Record<string, unknown> => typeof v === \"object\" && v !== null && !Array.isArray(v);\n const file = target ?? 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
@@ -7,17 +7,42 @@ export declare function userHarnessFile(harness: Harness): string;
7
7
  /** The project-level agent config file for a harness — what `init` writes, so the
8
8
  * hook travels with the repository rather than the machine. */
9
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. */
10
+ /** The personal, per-machine project config for a harness, or null when the harness
11
+ * has none. Claude Code reads `.claude/settings.local.json` beside `settings.json` and
12
+ * merges the hooks of both; it is the place for anything that names a path on this
13
+ * machine. Cursor and Codex have no such file. */
14
+ export declare function localHarnessFile(harness: Harness, cwd?: string): string | null;
15
+ /** Which config files actually carry a Scopebond hook for this harness. There are three
16
+ * places it can live — the shared project config, the personal project config (Claude
17
+ * Code's `settings.local.json`, where `init` pins the fast command) and the user config
18
+ * `install` writes — and `status`/`doctor` must look at all of them: checking only the
19
+ * user one told everyone who ran `init` that their install had failed. */
14
20
  export interface HarnessScopes {
15
21
  project: string | null;
22
+ local: string | null;
16
23
  user: string | null;
17
24
  }
18
25
  export declare function harnessScopes(harness: Harness, cwd?: string): HarnessScopes;
19
26
  /** One word for where a harness is wired, for a status line. */
20
27
  export declare function harnessScopeLabel(scopes: HarnessScopes): string;
28
+ /** Whether a hook command names a path that exists only on this machine — the pinned
29
+ * `"<node>" "<…/cli.js>" claude` form — rather than the portable `npx` form. */
30
+ export declare function isMachineSpecificCommand(command: string): boolean;
31
+ /** How git treats a config file: committed (`tracked`), kept out (`ignored`), neither
32
+ * yet (`untracked`, so a `git add .` would commit it), or not in a repository / no git
33
+ * on this machine (`none`). A file git shares must never carry a machine-specific
34
+ * command: on a teammate's machine the path does not exist, the command cannot start,
35
+ * and Claude Code, Cursor and Codex all treat a hook that cannot start as a non-blocking
36
+ * error — the agent runs with no check at all. */
37
+ export type GitShare = "tracked" | "ignored" | "untracked" | "none";
38
+ export declare function gitShareState(file: string): GitShare;
39
+ /** Keep a file out of git for this clone only, through `.git/info/exclude` — never the
40
+ * repository's own `.gitignore`, which is the team's file. Returns true when the file is
41
+ * ignored afterwards. */
42
+ export declare function excludeFromGit(file: string): boolean;
43
+ /** Remove the Scopebond entries whose command `remove` selects, leaving every other
44
+ * setting alone. Returns the number of entries removed. */
45
+ export declare function pruneHarnessEntries(file: string, remove: (command: string) => boolean): number;
21
46
  /** The file in the user home that pins trusted project policies: absolute project
22
47
  * config dir → SHA-256 of its policy.json. It lives under the protected home, so the
23
48
  * governed agent cannot write it. */
@@ -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;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"}
1
+ {"version":3,"file":"install.d.ts","sourceRoot":"","sources":["../src/install.ts"],"names":[],"mappings":"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"}
package/dist/install.js CHANGED
@@ -5,10 +5,11 @@
5
5
  // user-level install exists, a project-local `.scopebond` is used only if the user
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
+ import { execFileSync } from "node:child_process";
8
9
  import { createHash } from "node:crypto";
9
10
  import { mkdirSync, writeFileSync, existsSync, readFileSync, rmSync, copyFileSync } from "node:fs";
10
11
  import { homedir } from "node:os";
11
- import { dirname, join, resolve } from "node:path";
12
+ import { basename, dirname, isAbsolute, join, resolve } from "node:path";
12
13
  /** The user-level Scopebond home. `SCOPEBOND_HOME` overrides it (tests, CI). */
13
14
  export function userHome() {
14
15
  return process.env.SCOPEBOND_HOME ?? join(homedir(), ".scopebond");
@@ -31,23 +32,125 @@ export function projectHarnessFile(harness, cwd = process.cwd()) {
31
32
  return join(cwd, ".codex", "hooks.json");
32
33
  return join(cwd, ".claude", "settings.json");
33
34
  }
35
+ /** The personal, per-machine project config for a harness, or null when the harness
36
+ * has none. Claude Code reads `.claude/settings.local.json` beside `settings.json` and
37
+ * merges the hooks of both; it is the place for anything that names a path on this
38
+ * machine. Cursor and Codex have no such file. */
39
+ export function localHarnessFile(harness, cwd = process.cwd()) {
40
+ return harness === "claude" ? join(cwd, ".claude", "settings.local.json") : null;
41
+ }
34
42
  export function harnessScopes(harness, cwd = process.cwd()) {
35
43
  const project = projectHarnessFile(harness, cwd);
44
+ const local = localHarnessFile(harness, cwd);
36
45
  const user = userHarnessFile(harness);
37
46
  return {
38
47
  project: isHarnessConfigured(project) ? project : null,
48
+ local: local && isHarnessConfigured(local) ? local : null,
39
49
  user: isHarnessConfigured(user) ? user : null,
40
50
  };
41
51
  }
42
52
  /** One word for where a harness is wired, for a status line. */
43
53
  export function harnessScopeLabel(scopes) {
44
- if (scopes.project && scopes.user)
45
- return "configured (project + user)";
46
- if (scopes.project)
54
+ const where = [
55
+ scopes.project ? "project" : "",
56
+ scopes.local ? "this machine" : "",
57
+ scopes.user ? "user" : "",
58
+ ].filter(Boolean);
59
+ if (where.length === 0)
60
+ return "";
61
+ if (where.length === 1 && scopes.project)
47
62
  return "configured (this project)";
48
- if (scopes.user)
63
+ if (where.length === 1 && scopes.local)
64
+ return "configured (this project, this machine only)";
65
+ if (where.length === 1 && scopes.user)
49
66
  return "configured (user-level)";
50
- return "";
67
+ return `configured (${where.join(" + ")})`;
68
+ }
69
+ /** Whether a hook command names a path that exists only on this machine — the pinned
70
+ * `"<node>" "<…/cli.js>" claude` form — rather than the portable `npx` form. */
71
+ export function isMachineSpecificCommand(command) {
72
+ if (/(^|\s)npx(\.cmd)?\s/.test(command))
73
+ return false;
74
+ const first = (command.match(/"[^"]+"|\S+/) ?? [""])[0].replace(/^"|"$/g, "");
75
+ return isAbsolute(first) || /^[A-Za-z]:[\\/]/.test(first);
76
+ }
77
+ export function gitShareState(file) {
78
+ const cwd = dirname(file);
79
+ const git = (args) => {
80
+ try {
81
+ mkdirSync(cwd, { recursive: true });
82
+ execFileSync("git", args, { cwd, stdio: "ignore", timeout: 5000 });
83
+ return 0;
84
+ }
85
+ catch (error) {
86
+ const status = error.status;
87
+ return typeof status === "number" ? status : -1;
88
+ }
89
+ };
90
+ if (git(["rev-parse", "--is-inside-work-tree"]) !== 0)
91
+ return "none";
92
+ if (git(["ls-files", "--error-unmatch", "--", file]) === 0)
93
+ return "tracked";
94
+ return git(["check-ignore", "-q", "--", file]) === 0 ? "ignored" : "untracked";
95
+ }
96
+ /** Keep a file out of git for this clone only, through `.git/info/exclude` — never the
97
+ * repository's own `.gitignore`, which is the team's file. Returns true when the file is
98
+ * ignored afterwards. */
99
+ export function excludeFromGit(file) {
100
+ try {
101
+ const cwd = dirname(file);
102
+ mkdirSync(cwd, { recursive: true });
103
+ // The entry is built from git's own view of where this directory sits in the work
104
+ // tree (`--show-prefix`), not from comparing file-system paths: the same directory
105
+ // can be spelled two ways (a Windows 8.3 short name, a symlinked temp dir), and a
106
+ // path computed across the two spellings excludes the wrong file.
107
+ const prefix = execFileSync("git", ["rev-parse", "--show-prefix"], { cwd, encoding: "utf8", timeout: 5000 }).trim();
108
+ const exclude = resolve(cwd, execFileSync("git", ["rev-parse", "--git-path", "info/exclude"], { cwd, encoding: "utf8", timeout: 5000 }).trim());
109
+ const entry = "/" + prefix + basename(file);
110
+ const existing = existsSync(exclude) ? readFileSync(exclude, "utf8") : "";
111
+ if (!existing.split(/\r?\n/).includes(entry)) {
112
+ mkdirSync(dirname(exclude), { recursive: true });
113
+ writeFileSync(exclude, `${existing}${existing && !existing.endsWith("\n") ? "\n" : ""}${entry}\n`);
114
+ }
115
+ return gitShareState(file) === "ignored";
116
+ }
117
+ catch {
118
+ return false;
119
+ }
120
+ }
121
+ /** Remove the Scopebond entries whose command `remove` selects, leaving every other
122
+ * setting alone. Returns the number of entries removed. */
123
+ export function pruneHarnessEntries(file, remove) {
124
+ if (!existsSync(file))
125
+ return 0;
126
+ let config;
127
+ try {
128
+ config = readHarnessConfig(file);
129
+ }
130
+ catch {
131
+ return 0;
132
+ }
133
+ const hooks = isRecord(config.hooks) ? config.hooks : null;
134
+ if (!hooks)
135
+ return 0;
136
+ let removed = 0;
137
+ const commandsOf = (e) => {
138
+ if (!isRecord(e))
139
+ return [];
140
+ const own = isScopebond(e.command) ? [String(e.command)] : [];
141
+ const nested = Array.isArray(e.hooks) ? e.hooks.flatMap(commandsOf) : [];
142
+ return [...own, ...nested];
143
+ };
144
+ for (const [event, value] of Object.entries(hooks)) {
145
+ if (!Array.isArray(value))
146
+ continue;
147
+ const kept = value.filter((e) => !(entryMatches(e) && commandsOf(e).some(remove)));
148
+ removed += value.length - kept.length;
149
+ hooks[event] = kept;
150
+ }
151
+ if (removed > 0)
152
+ writeFileSync(file, JSON.stringify(config, null, 2) + "\n");
153
+ return removed;
51
154
  }
52
155
  /** The file in the user home that pins trusted project policies: absolute project
53
156
  * config dir → SHA-256 of its policy.json. It lives under the protected home, so the