@scopebond/hook 0.5.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 (54) hide show
  1. package/README.md +139 -12
  2. package/dist/cli.d.ts +2 -1
  3. package/dist/cli.d.ts.map +1 -1
  4. package/dist/cli.js +667 -69
  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 +9 -3
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +5 -2
  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 +36 -28
  21. package/dist/init.js.map +1 -1
  22. package/dist/install.d.ts +63 -3
  23. package/dist/install.d.ts.map +1 -1
  24. package/dist/install.js +242 -28
  25. package/dist/install.js.map +1 -1
  26. package/dist/map.d.ts +12 -3
  27. package/dist/map.d.ts.map +1 -1
  28. package/dist/map.js +765 -60
  29. package/dist/map.js.map +1 -1
  30. package/dist/minimize.d.ts +6 -3
  31. package/dist/minimize.d.ts.map +1 -1
  32. package/dist/minimize.js +24 -4
  33. package/dist/minimize.js.map +1 -1
  34. package/dist/rules.d.ts +51 -0
  35. package/dist/rules.d.ts.map +1 -0
  36. package/dist/rules.js +216 -0
  37. package/dist/rules.js.map +1 -0
  38. package/dist/runtime-install.d.ts +25 -0
  39. package/dist/runtime-install.d.ts.map +1 -0
  40. package/dist/runtime-install.js +106 -0
  41. package/dist/runtime-install.js.map +1 -0
  42. package/dist/runtime.d.ts +17 -0
  43. package/dist/runtime.d.ts.map +1 -1
  44. package/dist/runtime.js +122 -11
  45. package/dist/runtime.js.map +1 -1
  46. package/dist/shell.d.ts +53 -4
  47. package/dist/shell.d.ts.map +1 -1
  48. package/dist/shell.js +737 -81
  49. package/dist/shell.js.map +1 -1
  50. package/dist/version.d.ts +5 -1
  51. package/dist/version.d.ts.map +1 -1
  52. package/dist/version.js +6 -2
  53. package/dist/version.js.map +1 -1
  54. package/package.json +3 -3
package/README.md CHANGED
@@ -23,8 +23,11 @@ scaffolds a user-level home (`~/.scopebond`, override `SCOPEBOND_HOME`) with a
23
23
  signing key, a countersigning key and a starter policy, and registers the hook by
24
24
  absolute path in your user-level `~/.claude/settings.json`, `~/.cursor/hooks.json`,
25
25
  or `~/.codex/hooks.json`. After Codex setup, open Codex, run `/hooks`, review
26
- Scopebond, and choose **Trust** once. Every project you open is then governed, and a project-local
27
- `.scopebond/` still takes precedence when you want a per-repo policy.
26
+ Scopebond, and choose **Trust** once. Every project you open is then governed by your
27
+ user-level policy. A project's own `.scopebond/policy.json` applies only after you trust it
28
+ in that project (`scopebond trust`, or `init` there), pinned to its exact contents: a
29
+ repository you clone, or an agent working in it, cannot swap in a weaker policy, and any
30
+ later edit to that file stops it applying until you trust it again.
28
31
 
29
32
  ```
30
33
  scopebond status # home, which agents are configured, cloud, receipts
@@ -54,7 +57,62 @@ npx @scopebond/hook init --codex # Codex, then approve it once with /hooks
54
57
  countersigning key, a starter policy — "protect main and production paths" — and a
55
58
  `.gitignore` so none of it is committed) and configures `.claude/settings.json`,
56
59
  `.cursor/hooks.json`, or `.codex/hooks.json`. Then run one safe command in the agent and see the receipt in
57
- `.scopebond/receipts.db`.
60
+ `.scopebond/receipts.db`. `init`, `trust` and `uninstall` are meant for a person at a
61
+ terminal: in a script or CI, pass `--yes`.
62
+
63
+ Check what it did with `npx @scopebond/hook status` (which agents are configured, in
64
+ which scope) and `npx @scopebond/hook doctor` (whether each configured command can
65
+ actually start).
66
+
67
+ ### Changing the rules
68
+
69
+ `.scopebond/policy.json` is **generated**. The thing you edit is `.scopebond/rules.json`,
70
+ a short readable list, and `policy.json` is compiled from it — so a limit is a line in a
71
+ list, not a 700-character lookahead:
72
+
73
+ ```
74
+ npx @scopebond/hook rules # what is blocked, in plain English
75
+ npx @scopebond/hook rules allow dd # stop blocking a program
76
+ npx @scopebond/hook rules protect infra/ # never write there
77
+ npx @scopebond/hook rules protect-branch production
78
+ npx @scopebond/hook rules apply # recompile after editing rules.json by hand
79
+ ```
80
+
81
+ The compiled patterns are identical to the ones this package has always shipped — there
82
+ is a test that pins them against the starter policy — so the readable front end cannot
83
+ change what is enforced. Each clause description is generated from the list too, so it
84
+ stays true after an edit, and a block message quotes it.
85
+
86
+ ### The hook command `init` installs
87
+
88
+ The hook runs once per tool call, so the command has to start fast. `init` copies this
89
+ package into `~/.scopebond/runtime/<version>/` once per machine and points your agent
90
+ at that absolute path. Measured on one Windows machine, through a shell, warm cache:
91
+
92
+ | Command in the agent config | Median per tool call |
93
+ |---|---|
94
+ | pinned absolute path (the default) | **151 ms** |
95
+ | `npx -y @scopebond/hook@<version>` | 1053 ms |
96
+
97
+ That ~900 ms is npm re-resolving a package already on disk, on every action. Pass
98
+ `--npx` to `init` if you would rather have the portable command, and `init` falls back
99
+ to it automatically when it cannot make a durable copy — slow beats broken. `doctor`
100
+ re-checks that the pinned paths still resolve, so a cleared home or a switched Node
101
+ version shows up as a problem rather than as a hook that silently cannot start.
102
+
103
+ ### What each agent can actually stop
104
+
105
+ | | Claude Code | Cursor | Codex |
106
+ |---|---|---|---|
107
+ | Shell commands | prevented | prevented | prevented |
108
+ | File reads | prevented | prevented | prevented |
109
+ | MCP tool calls | prevented | prevented | prevented |
110
+ | File writes / edits | prevented | **recorded, not prevented** | prevented |
111
+
112
+ Cursor reports a file edit only *after* it is written (`afterFileEdit`; it has no
113
+ before-edit hook), so an out-of-policy edit there is signed and flagged, not blocked —
114
+ and the message says so rather than claiming otherwise. For edits that must be stopped
115
+ before they land, make `@scopebond/github-action` a required check on pull requests.
58
116
 
59
117
  ## Connect it to your workspace (optional)
60
118
 
@@ -78,8 +136,8 @@ can hand you a single copy-paste command with nothing to save.
78
136
 
79
137
  From then on every receipt is mirrored to the workspace through a **durable outbox**:
80
138
  delivery is best-effort and never blocks a tool call, and receipts are retained
81
- locally and retried if the workspace is unreachable. `scopebond-hook flush` delivers
82
- anything still queued — run it on a session-end hook (and set
139
+ locally and retried if the workspace is unreachable. `npx @scopebond/hook flush`
140
+ delivers anything still queued — run it on a session-end hook (and set
83
141
  `SCOPEBOND_HOOK_FLUSH_MS=0`) if you want zero per-call latency.
84
142
 
85
143
  ## How it works
@@ -87,17 +145,86 @@ anything still queued — run it on a session-end hook (and set
87
145
  Each tool call is mapped to a normalized [Action Taxonomy](https://github.com/avouro-com/scopebond)
88
146
  action (`shell.exec`, `git.push`, `file.write`, `mcp.tool.call`, …), signed by the
89
147
  machine key and decided against your policy by an in-process check-only gateway.
90
- An allowed action is recorded and left to the coding agent's normal permission
91
- prompt; the hook never auto-approves it. A denied action is blocked. Unknown tools are
92
- recorded as *not evaluated* and grant nothing. Anything unexpected fails closed
93
- (deny) with a repair message.
148
+ An allowed action is recorded and the agent proceeds. A denied action is blocked.
149
+ Unknown tools are recorded as *not evaluated* and grant nothing — they fall through to
150
+ the coding agent's own permission prompt, so the hook never turns an unrecognised
151
+ action into an approval. Anything unexpected fails closed (deny) with a repair message.
152
+
153
+ **What a block says.** A denial names the action, the rule that decided, why that rule
154
+ exists (the clause's own description), the engine's technical detail and where to change
155
+ it. The same text goes to the agent, so it can choose another approach instead of
156
+ retrying a blocked call:
157
+
158
+ ```
159
+ Scopebond blocked git.push origin main — rule "protect-branches" (enforce).
160
+ Why: Deny pushes to main, master and release/* (any case, any refspec spelling), …
161
+ Detail: param ref fails pattern
162
+ Change the rule: edit clause "protect-branches" in /repo/.scopebond/policy.json
163
+ ```
94
164
 
95
165
  Commands are stored as a scrubbed head plus a digest; file contents are never
96
166
  stored; common secret shapes are removed before signing.
97
167
 
98
- **Strict mode.** By default a tool with no taxonomy mapping is recorded *not
99
- evaluated* (not blocked). Add `--strict` (or `SCOPEBOND_HOOK_STRICT=1`) to deny
100
- unmapped tools too — fail-closed coverage for anything the taxonomy does not map.
168
+ **Where receipts live, and how much room they take.** `.scopebond/receipts.db` in the
169
+ project, roughly 25 KiB per tool call. Nothing is ever deleted automatically — these are
170
+ your evidence — so `status` reports the count and size, and `prune` bounds it when you
171
+ choose to:
172
+
173
+ ```
174
+ npx @scopebond/hook prune # report the footprint
175
+ npx @scopebond/hook prune --before 90d --yes # archive, then remove, anything older
176
+ ```
177
+
178
+ `prune` writes the receipts it will remove to a JSONL file beside the database first, so
179
+ they stay verifiable, and it refuses outright once the log has been anchored — a receipt's
180
+ position is its anchor leaf index, so removing one would make an existing anchor
181
+ unverifiable.
182
+
183
+ **What a shell command is checked for.** A command line is split into every command it
184
+ runs (`&&`, `;`, pipes, `$( )` and backticks — also inside double quotes — `bash -c`,
185
+ `eval`, `trap`, `su -c`, `script -c`, `watch`, `parallel`, `cmd /c`, `pwsh -Command`
186
+ and `-EncodedCommand`, `find -exec`, `env -S`), with shell keywords and grouping
187
+ (`if … then`, `for … do`, `{ … }`, `!`, `case`) and wrappers (`sudo`, `env`, `time`,
188
+ `nice`, `xargs`, `timeout`, `strace`, `busybox` …, each with its own option arity)
189
+ stripped so the real program is seen; the wrappers are checked too. Each command is
190
+ checked as a program, and the files it reads or writes are checked like the Read and
191
+ Write tools: operands of programs that output or copy file content (`cat`, `grep`,
192
+ `tar`, …), copies and moves (`cp`, `mv`, `rsync`, `scp`, `Copy-Item`), writers and
193
+ editors (`tee`, `touch`, `sed -i`, `yq -i`, `ed`, `vim`, `Set-Content`), git's own writes
194
+ (`checkout -- p`, `restore`, `mv`, `rm`, `config core.hooksPath`), secrets sent or staged
195
+ (`curl -T`, `-d @file`, `gh gist create`, `git add`) and redirections, including
196
+ `x>file` without spaces. Existence and metadata checks (`ls`, `test -f`, `[ -f ]`,
197
+ `stat`) and a secret file's name in a string are not reads. Paths are compared
198
+ case-insensitively; Windows `\` paths, 8.3 short names (`CLAUDE~1`), `::$DATA` streams
199
+ and trailing dots are normalized; and a glob, variable or brace list that could name a
200
+ protected file or directory (`.scope*/agent.key`, `.*/*`, `$HOME/.ssh/id_rsa`) is
201
+ treated as that file. Every destination of a `git push` is checked in the common
202
+ spellings (`refs/heads/main`, `HEAD:main`, a second refspec, `--repo`, `--all`,
203
+ `--mirror`); a push whose destination is not on the command line (a git alias, a
204
+ configured push refspec, `send-pack`) is denied, and a tags-only push is allowed. The
205
+ agent running the hook's own `init`, `install`, `trust`, `uninstall` or `connect` is
206
+ denied, and those commands also refuse a non-interactive terminal unless `--yes` is
207
+ passed.
208
+
209
+ **Limits.** The hook sees the command text, not what a program does at run time. It
210
+ does not follow a variable whose value it cannot see (`cat $FILE`), a path assembled
211
+ inside a script or interpreter (`python script.py`), aliases and functions defined in
212
+ an earlier call, git aliases from a config file, recursive reads of a parent of a
213
+ protected directory (`grep -r . `, `cp -r ~ /tmp`), or deletion expressed as arguments
214
+ (`find -delete`, `git clean`). Commands whose written files are named only inside their
215
+ input — `patch`, `git apply`, `git am`, `tar x`, `unzip`, `7z x`, `cpio -i` — are
216
+ recorded with an unevaluated write: allowed in normal mode, denied in strict mode.
217
+ Reading any `*.pem` file is denied, including a public certificate, because a PEM file
218
+ is often a private key. For stronger guarantees, run the agent behind the Scopebond
219
+ gateway or in a sandbox; the hook is a guardrail for a cooperating agent, not a jail.
220
+
221
+ **Strict mode.** A shell command that cannot be parsed (an unbalanced quote) or whose
222
+ program is only known at run time (`$cmd`, `$(…) args`) is checked with an empty
223
+ program name, which the starter policy denies in every mode — edit the `safe-shell`
224
+ clause to change that. By default a tool with no taxonomy mapping, or a write whose
225
+ target the command text does not name, is recorded *not evaluated* (not blocked). Add
226
+ `--strict` (or `SCOPEBOND_HOOK_STRICT=1`) to deny those too — fail-closed coverage for
227
+ anything the taxonomy does not map.
101
228
 
102
229
  ## Library
103
230
 
package/dist/cli.d.ts CHANGED
@@ -1,3 +1,4 @@
1
1
  #!/usr/bin/env node
2
- export {};
2
+ /** A `--since` value: a duration (`7d`, `24h`, `30m`) or an ISO-ish date. */
3
+ export declare function parseSince(value: string | undefined, now?: number): number | null;
3
4
  //# sourceMappingURL=cli.d.ts.map
package/dist/cli.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":""}
1
+ {"version":3,"file":"cli.d.ts","sourceRoot":"","sources":["../src/cli.ts"],"names":[],"mappings":";AA8fA,6EAA6E;AAC7E,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,EAAE,GAAG,GAAE,MAAmB,GAAG,MAAM,GAAG,IAAI,CAS7F"}