@scopebond/hook 0.15.1 → 0.16.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.
package/README.md CHANGED
@@ -266,6 +266,15 @@ patterns: the hook compiles its choices with the same compiler as `rules apply`.
266
266
  - **What the workspace cannot change.** Protection of Scopebond's own settings and of the
267
267
  agents' hook settings, the machine key policy, fail-closed handling of anything unreadable,
268
268
  and this computer's own opt-ins (`allowed_roots`, `protect_remote_database`).
269
+ - **Overrides.** The workspace can set a rule to *Block, user may override* and say who may
270
+ override. A matching action is then blocked until the person at the computer allows it once,
271
+ with a reason, in the Scopebond Agent's window (`@scopebond/agent`): the hook asks the running
272
+ agent and waits up to 45 seconds (20 seconds for Codex and Cursor), then blocks. The receipt
273
+ records the override, the reason's digest and the rule; the workspace adds who and why. Where the
274
+ workspace allows it and the window is not available, Claude Code's own prompt is offered instead,
275
+ but only in a permission mode where Claude Code really asks; that prompt takes no reason and is
276
+ recorded as *offered*. Scopebond's own protection, rules set to Block and the kill switch are
277
+ never overridable, and the workspace's daily limit holds.
269
278
  - **Going back.** While the workspace sets the rules, `rules` edits and `policy load` are
270
279
  refused here. If the connection is revoked, or the workspace stops setting rules for this
271
280
  computer, the hook recompiles `policy.json` from `rules.json`: a computer is never left
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AAoqBA,6EAA6E;AAC7E,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,GAAG,GAAE,MAAmB,GAAG,MAAM,GAAG,IAAI,CAS7F"}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AA+rBA,6EAA6E;AAC7E,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,GAAG,GAAE,MAAmB,GAAG,MAAM,GAAG,IAAI,CAS7F"}
package/dist/cli.js CHANGED
@@ -35,6 +35,7 @@ import { mapClaudeToolUse, mapCodexToolUse, mapCursorEvent, fillPushBranch } fro
35
35
  import { createHookRuntime } from "./runtime.js";
36
36
  import { useDigestKey, loadOrCreateDigestKey } from "./minimize.js";
37
37
  import { scaffold, harnessSnippet, placeHook } from "./init.js";
38
+ import { onboardingSteps } from "./onboarding.js";
38
39
  import { userHome, userHarnessFile, resolveConfigDir, writeHarnessConfig, removeHarnessConfig, cursorDetected, codexDetected, absoluteHookCommand, isHarnessConfigured, purgeHome, harnessScopes, harnessScopeLabel, configuredHookCommands, hookCommandResolves, projectHarnessFile, localHarnessFile, gitShareState, isMachineSpecificCommand, trustProjectPolicy, untrustedProjectPolicy, isTrustedProject, wireLifecycleHooks, unwireLifecycleHooks, } from "./install.js";
39
40
  import { openObservations, describeObservations, observationStatus, stopReasonFromClaude, exitFromClaudeFailure, HEARTBEAT_INTERVAL_MS, OBSERVATIONS_SCOPE, } from "./obs-emitter.js";
40
41
  import { OBSERVATION_DB, ObservationStore } from "./obs-store.js";
@@ -44,6 +45,7 @@ import { connectCloud, ingestUrl, loadConnection } from "./cloud.js";
44
45
  import { recoverEarlierReceipts } from "./recover.js";
45
46
  import { loadPolicyExport, policyBuilds } from "./policy-load.js";
46
47
  import { isManaged, readMeta, MANAGED_DOC_FILE } from "./managed.js";
48
+ import { createOverrideHandler, overrideHint } from "./override.js";
47
49
  import { syncIfDue, syncPolicy } from "./policy-sync.js";
48
50
  import { loadBudgetExport } from "./budget-load.js";
49
51
  import { compile, defaultRules, describeRules, loadRules, saveRules, rulesPath, pathRuleFor } from "./rules.js";
@@ -128,6 +130,17 @@ function readStdin() {
128
130
  return "";
129
131
  }
130
132
  }
133
+ /** How long a hook waits for a person in the Scopebond window: well inside each agent's hook timeout (Claude Code 60 s by
134
+ * default; the Codex entry Scopebond writes says 30 s), so the wait ends as a block, never as a timed-out hook. */
135
+ const OVERRIDE_WAIT_MS = { claude: 45_000, codex: 20_000, cursor: 20_000 };
136
+ /** Warn mode: Claude Code shows the person its own prompt for this action (only where the workspace allows it and Claude Code
137
+ * really asks; see override.ts). */
138
+ function askClaude(reason) {
139
+ process.stdout.write(JSON.stringify({
140
+ hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "ask", permissionDecisionReason: reason },
141
+ }) + "\n");
142
+ process.exit(0);
143
+ }
131
144
  function denyClaude(reason) {
132
145
  process.stdout.write(JSON.stringify({
133
146
  hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: reason },
@@ -217,7 +230,11 @@ async function runPreToolUse(mapper, deny = denyClaude, raw, harness = "claude")
217
230
  try {
218
231
  const cwd = input?.cwd ? String(input.cwd) : process.cwd();
219
232
  const dir = resolveConfigDir(cwd);
220
- runtime = createHookRuntime(runtimePaths(dir, cwd));
233
+ const permissionMode = typeof input.permission_mode === "string" ? input.permission_mode : null;
234
+ runtime = createHookRuntime({ ...runtimePaths(dir, cwd), override: (agentKid) => {
235
+ const made = createOverrideHandler({ dir, home: userHome(), agentKid, harness, permissionMode, waitMs: OVERRIDE_WAIT_MS[harness] });
236
+ return made ? { handler: made.handler, hint: () => overrideHint(made.note()) } : null;
237
+ } });
221
238
  useDigestKey(loadOrCreateDigestKey(dir));
222
239
  const decision = await runtime.evaluate([...fillPushBranch(mapper(input), currentBranch(cwd)), ...databaseGuard(dir, cwd, input)], { groupKey: callId(input) });
223
240
  const observer = recordObservations(dir, cwd, input, decision, harness);
@@ -230,6 +247,12 @@ async function runPreToolUse(mapper, deny = denyClaude, raw, harness = "claude")
230
247
  runtime = undefined;
231
248
  if (decision.decision === "deny")
232
249
  deny(decision.reason);
250
+ // Only Claude Code is ever offered "ask" (override.ts); any other harness treats it as a denial.
251
+ if (decision.decision === "ask") {
252
+ if (harness === "claude")
253
+ askClaude(decision.reason);
254
+ deny(decision.reason);
255
+ }
233
256
  // Stay silent on allow/not_evaluated so the coding agent's normal permission
234
257
  // flow remains in charge. Scopebond blocks; it never silently approves.
235
258
  process.exit(0);
@@ -328,9 +351,15 @@ async function runCursor() {
328
351
  try {
329
352
  const cwd = input?.cwd ? String(input.cwd) : process.cwd();
330
353
  const dir = resolveConfigDir(cwd);
331
- runtime = createHookRuntime(runtimePaths(dir, cwd));
332
- useDigestKey(loadOrCreateDigestKey(dir));
354
+ const permissionMode = typeof input.permission_mode === "string" ? input.permission_mode : null;
333
355
  const mapped = fillPushBranch(mapCursorEvent(event, input), currentBranch(cwd));
356
+ // An edit Cursor reports after saving it cannot be overridden: it already happened.
357
+ const overridable = !mapped.some((m) => m.postHoc);
358
+ runtime = createHookRuntime({ ...runtimePaths(dir, cwd), override: (agentKid) => {
359
+ const made = overridable ? createOverrideHandler({ dir, home: userHome(), agentKid, harness: "cursor", permissionMode, waitMs: OVERRIDE_WAIT_MS.cursor }) : null;
360
+ return made ? { handler: made.handler, hint: () => overrideHint(made.note()) } : null;
361
+ } });
362
+ useDigestKey(loadOrCreateDigestKey(dir));
334
363
  const decision = await runtime.evaluate(mapped, { groupKey: callId(input) });
335
364
  const observer = recordObservations(dir, cwd, input, decision, "cursor");
336
365
  await Promise.all([runtime.flush(), observer?.flush() ?? Promise.resolve(), syncIfDue(dir, () => syncOptionsFor(dir))]);
@@ -481,7 +510,7 @@ function runInit(args) {
481
510
  // `scopebond-hook` binary on PATH, so a bare `scopebond-hook log` would fail.
482
511
  console.log(`Next: run one command in the agent, then \`${cliCommand("log")}\` to see the receipt`);
483
512
  console.log(`and \`${cliCommand("verify")}\` to check it offline. Try \`${cliCommand('test "rm -rf /"')}\`.`);
484
- console.log(`To send receipts to a workspace: ${cliCommand("connect <workspace-url> <enrollment>")}`);
513
+ console.log(onboardingSteps({ harness, command: cliCommand, project: true }).join("\n"));
485
514
  }
486
515
  function decisionOf(payload) {
487
516
  const rr = String(payload.realtime_result ?? "");
@@ -956,6 +985,7 @@ async function finishConnect(dir, url, bundle, harness, args) {
956
985
  console.log(`Observations: ${observing.reason}`);
957
986
  console.log("");
958
987
  console.log("Run your agent — the first action appears in your workspace within seconds.");
988
+ console.log(onboardingSteps({ harness, command: cliCommand, connected: true }).join("\n"));
959
989
  }
960
990
  catch (error) {
961
991
  const message = error.message;
@@ -1355,7 +1385,7 @@ function runInstall(args) {
1355
1385
  console.log("");
1356
1386
  console.log(`A project's own .scopebond policy applies only after you trust it there (${cliCommand("trust")}).`);
1357
1387
  console.log(`Check it: ${cliCommand("doctor")} · see decisions: ${cliCommand("log")}`);
1358
- console.log(`To send receipts to a workspace: ${cliCommand("connect <workspace-url> <enrollment>")}`);
1388
+ console.log(onboardingSteps({ harness: harnesses[0], command: cliCommand }).join("\n"));
1359
1389
  }
1360
1390
  /** Size and count of the local receipt store, plus its write-ahead log. Signed evidence
1361
1391
  * accumulates in the user's project directory and there is no automatic deletion — so