@guilz-dev/belay 0.1.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 (266) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +268 -0
  3. package/agent-belay-logo.png +0 -0
  4. package/dist/adapters/claude/adapter.d.ts +7 -0
  5. package/dist/adapters/claude/adapter.js +114 -0
  6. package/dist/adapters/claude/hooks.d.ts +13 -0
  7. package/dist/adapters/claude/hooks.js +49 -0
  8. package/dist/adapters/claude/runtime-entry.d.ts +4 -0
  9. package/dist/adapters/claude/runtime-entry.js +260 -0
  10. package/dist/adapters/codex/adapter.d.ts +7 -0
  11. package/dist/adapters/codex/adapter.js +73 -0
  12. package/dist/adapters/codex/hooks.d.ts +21 -0
  13. package/dist/adapters/codex/hooks.js +78 -0
  14. package/dist/adapters/codex/runtime-entry.d.ts +4 -0
  15. package/dist/adapters/codex/runtime-entry.js +237 -0
  16. package/dist/adapters/cursor/adapter.d.ts +7 -0
  17. package/dist/adapters/cursor/adapter.js +29 -0
  18. package/dist/adapters/cursor/hooks.d.ts +2 -0
  19. package/dist/adapters/cursor/hooks.js +26 -0
  20. package/dist/adapters/cursor/runtime-entry.d.ts +4 -0
  21. package/dist/adapters/cursor/runtime-entry.js +143 -0
  22. package/dist/adapters/layouts/claude.d.ts +2 -0
  23. package/dist/adapters/layouts/claude.js +40 -0
  24. package/dist/adapters/layouts/codex.d.ts +2 -0
  25. package/dist/adapters/layouts/codex.js +43 -0
  26. package/dist/adapters/layouts/cursor.d.ts +2 -0
  27. package/dist/adapters/layouts/cursor.js +40 -0
  28. package/dist/adapters/layouts/index.d.ts +7 -0
  29. package/dist/adapters/layouts/index.js +23 -0
  30. package/dist/adapters/layouts/protected-paths.d.ts +3 -0
  31. package/dist/adapters/layouts/protected-paths.js +15 -0
  32. package/dist/adapters/layouts/scope.d.ts +19 -0
  33. package/dist/adapters/layouts/scope.js +86 -0
  34. package/dist/adapters/layouts/types.d.ts +14 -0
  35. package/dist/adapters/layouts/types.js +1 -0
  36. package/dist/adapters/registry.d.ts +4 -0
  37. package/dist/adapters/registry.js +14 -0
  38. package/dist/adapters/shared/gate-runtime.d.ts +51 -0
  39. package/dist/adapters/shared/gate-runtime.js +518 -0
  40. package/dist/adapters/shared/repo-root.d.ts +2 -0
  41. package/dist/adapters/shared/repo-root.js +17 -0
  42. package/dist/adapters/types.d.ts +19 -0
  43. package/dist/adapters/types.js +1 -0
  44. package/dist/branding.d.ts +3 -0
  45. package/dist/branding.js +3 -0
  46. package/dist/bundle/claude-runtime.mjs +5323 -0
  47. package/dist/bundle/codex-runtime.mjs +5310 -0
  48. package/dist/bundle/cursor-runtime.mjs +5208 -0
  49. package/dist/cleanup-orphans.d.ts +7 -0
  50. package/dist/cleanup-orphans.js +59 -0
  51. package/dist/cli.d.ts +2 -0
  52. package/dist/cli.js +631 -0
  53. package/dist/commands/approve.d.ts +14 -0
  54. package/dist/commands/approve.js +65 -0
  55. package/dist/commands/audit.d.ts +59 -0
  56. package/dist/commands/audit.js +132 -0
  57. package/dist/commands/classify-for-report.d.ts +9 -0
  58. package/dist/commands/classify-for-report.js +85 -0
  59. package/dist/commands/doctor.d.ts +3 -0
  60. package/dist/commands/doctor.js +366 -0
  61. package/dist/commands/dogfood.d.ts +5 -0
  62. package/dist/commands/dogfood.js +71 -0
  63. package/dist/commands/explain.d.ts +3 -0
  64. package/dist/commands/explain.js +133 -0
  65. package/dist/commands/health-snapshot.d.ts +2 -0
  66. package/dist/commands/health-snapshot.js +166 -0
  67. package/dist/commands/init-wizard.d.ts +16 -0
  68. package/dist/commands/init-wizard.js +50 -0
  69. package/dist/commands/metrics.d.ts +7 -0
  70. package/dist/commands/metrics.js +89 -0
  71. package/dist/commands/recover.d.ts +3 -0
  72. package/dist/commands/recover.js +105 -0
  73. package/dist/commands/report.d.ts +3 -0
  74. package/dist/commands/report.js +65 -0
  75. package/dist/commands/revoke.d.ts +5 -0
  76. package/dist/commands/revoke.js +22 -0
  77. package/dist/commands/simulate.d.ts +14 -0
  78. package/dist/commands/simulate.js +55 -0
  79. package/dist/commands/status.d.ts +5 -0
  80. package/dist/commands/status.js +107 -0
  81. package/dist/config-io.d.ts +23 -0
  82. package/dist/config-io.js +180 -0
  83. package/dist/conformance/guarantee-table.d.ts +14 -0
  84. package/dist/conformance/guarantee-table.js +95 -0
  85. package/dist/conformance/types.d.ts +6 -0
  86. package/dist/conformance/types.js +1 -0
  87. package/dist/core/approval-service.d.ts +26 -0
  88. package/dist/core/approval-service.js +41 -0
  89. package/dist/core/approval-token.d.ts +11 -0
  90. package/dist/core/approval-token.js +61 -0
  91. package/dist/core/approval.d.ts +19 -0
  92. package/dist/core/approval.js +58 -0
  93. package/dist/core/audit-analysis.d.ts +10 -0
  94. package/dist/core/audit-analysis.js +147 -0
  95. package/dist/core/audit-metrics.d.ts +51 -0
  96. package/dist/core/audit-metrics.js +155 -0
  97. package/dist/core/audit-query.d.ts +11 -0
  98. package/dist/core/audit-query.js +142 -0
  99. package/dist/core/audit-summary.d.ts +33 -0
  100. package/dist/core/audit-summary.js +111 -0
  101. package/dist/core/audit-types.d.ts +65 -0
  102. package/dist/core/audit-types.js +2 -0
  103. package/dist/core/capability/allowlist.d.ts +10 -0
  104. package/dist/core/capability/allowlist.js +53 -0
  105. package/dist/core/capability/broker.d.ts +17 -0
  106. package/dist/core/capability/broker.js +29 -0
  107. package/dist/core/capability/index.d.ts +5 -0
  108. package/dist/core/capability/index.js +4 -0
  109. package/dist/core/capability/paths.d.ts +1 -0
  110. package/dist/core/capability/paths.js +20 -0
  111. package/dist/core/capability/reasons.d.ts +2 -0
  112. package/dist/core/capability/reasons.js +4 -0
  113. package/dist/core/capability/types.d.ts +10 -0
  114. package/dist/core/capability/types.js +1 -0
  115. package/dist/core/capability-approval.d.ts +28 -0
  116. package/dist/core/capability-approval.js +43 -0
  117. package/dist/core/classify-subagent.d.ts +2 -0
  118. package/dist/core/classify-subagent.js +69 -0
  119. package/dist/core/classify-tool.d.ts +3 -0
  120. package/dist/core/classify-tool.js +311 -0
  121. package/dist/core/config-layers.d.ts +23 -0
  122. package/dist/core/config-layers.js +59 -0
  123. package/dist/core/config.d.ts +219 -0
  124. package/dist/core/config.js +720 -0
  125. package/dist/core/control-plane-isolation.d.ts +10 -0
  126. package/dist/core/control-plane-isolation.js +83 -0
  127. package/dist/core/custom-command-match.d.ts +2 -0
  128. package/dist/core/custom-command-match.js +8 -0
  129. package/dist/core/egress/allowlist.d.ts +7 -0
  130. package/dist/core/egress/allowlist.js +33 -0
  131. package/dist/core/egress/env.d.ts +3 -0
  132. package/dist/core/egress/env.js +17 -0
  133. package/dist/core/egress/fingerprint.d.ts +3 -0
  134. package/dist/core/egress/fingerprint.js +35 -0
  135. package/dist/core/egress/policy.d.ts +8 -0
  136. package/dist/core/egress/policy.js +47 -0
  137. package/dist/core/egress/proxy-server.d.ts +21 -0
  138. package/dist/core/egress/proxy-server.js +263 -0
  139. package/dist/core/egress/types.d.ts +25 -0
  140. package/dist/core/egress/types.js +1 -0
  141. package/dist/core/egress-approval.d.ts +48 -0
  142. package/dist/core/egress-approval.js +129 -0
  143. package/dist/core/fingerprint.d.ts +6 -0
  144. package/dist/core/fingerprint.js +24 -0
  145. package/dist/core/gate-contract.d.ts +48 -0
  146. package/dist/core/gate-contract.js +50 -0
  147. package/dist/core/gate-engine.d.ts +20 -0
  148. package/dist/core/gate-engine.js +172 -0
  149. package/dist/core/glob.d.ts +1 -0
  150. package/dist/core/glob.js +39 -0
  151. package/dist/core/index.d.ts +19 -0
  152. package/dist/core/index.js +15 -0
  153. package/dist/core/integrity.d.ts +15 -0
  154. package/dist/core/integrity.js +68 -0
  155. package/dist/core/judge-api-key.d.ts +4 -0
  156. package/dist/core/judge-api-key.js +11 -0
  157. package/dist/core/judge-config.d.ts +29 -0
  158. package/dist/core/judge-config.js +85 -0
  159. package/dist/core/judge-doctor.d.ts +7 -0
  160. package/dist/core/judge-doctor.js +124 -0
  161. package/dist/core/judgment.d.ts +6 -0
  162. package/dist/core/judgment.js +38 -0
  163. package/dist/core/notify.d.ts +13 -0
  164. package/dist/core/notify.js +44 -0
  165. package/dist/core/path-utils.d.ts +12 -0
  166. package/dist/core/path-utils.js +107 -0
  167. package/dist/core/reclassify.d.ts +15 -0
  168. package/dist/core/reclassify.js +82 -0
  169. package/dist/core/recover-advice.d.ts +30 -0
  170. package/dist/core/recover-advice.js +177 -0
  171. package/dist/core/recover-git-probe.d.ts +8 -0
  172. package/dist/core/recover-git-probe.js +50 -0
  173. package/dist/core/recover-select.d.ts +10 -0
  174. package/dist/core/recover-select.js +60 -0
  175. package/dist/core/scrub.d.ts +3 -0
  176. package/dist/core/scrub.js +87 -0
  177. package/dist/core/shell-substitution.d.ts +6 -0
  178. package/dist/core/shell-substitution.js +130 -0
  179. package/dist/core/shell-tokenizer.d.ts +5 -0
  180. package/dist/core/shell-tokenizer.js +129 -0
  181. package/dist/core/shell-unparseable.d.ts +4 -0
  182. package/dist/core/shell-unparseable.js +96 -0
  183. package/dist/core/transactional/diff-evaluator.d.ts +2 -0
  184. package/dist/core/transactional/diff-evaluator.js +84 -0
  185. package/dist/core/transactional/eligibility.d.ts +4 -0
  186. package/dist/core/transactional/eligibility.js +44 -0
  187. package/dist/core/transactional/git-worktree.d.ts +13 -0
  188. package/dist/core/transactional/git-worktree.js +189 -0
  189. package/dist/core/transactional/index.d.ts +5 -0
  190. package/dist/core/transactional/index.js +4 -0
  191. package/dist/core/transactional/reasons.d.ts +4 -0
  192. package/dist/core/transactional/reasons.js +8 -0
  193. package/dist/core/transactional/runner.d.ts +2 -0
  194. package/dist/core/transactional/runner.js +113 -0
  195. package/dist/core/transactional/types.d.ts +46 -0
  196. package/dist/core/transactional/types.js +1 -0
  197. package/dist/core/types.d.ts +90 -0
  198. package/dist/core/types.js +1 -0
  199. package/dist/core/v2/adapter.d.ts +14 -0
  200. package/dist/core/v2/adapter.js +118 -0
  201. package/dist/core/v2/containment.d.ts +19 -0
  202. package/dist/core/v2/containment.js +91 -0
  203. package/dist/core/v2/egress-classify.d.ts +7 -0
  204. package/dist/core/v2/egress-classify.js +216 -0
  205. package/dist/core/v2/fingerprint.d.ts +1 -0
  206. package/dist/core/v2/fingerprint.js +4 -0
  207. package/dist/core/v2/index.d.ts +12 -0
  208. package/dist/core/v2/index.js +10 -0
  209. package/dist/core/v2/judge-audit.d.ts +2 -0
  210. package/dist/core/v2/judge-audit.js +15 -0
  211. package/dist/core/v2/judge-factory.d.ts +25 -0
  212. package/dist/core/v2/judge-factory.js +75 -0
  213. package/dist/core/v2/judge-outbound.d.ts +12 -0
  214. package/dist/core/v2/judge-outbound.js +73 -0
  215. package/dist/core/v2/judge.d.ts +47 -0
  216. package/dist/core/v2/judge.js +264 -0
  217. package/dist/core/v2/launcher-resolve.d.ts +12 -0
  218. package/dist/core/v2/launcher-resolve.js +190 -0
  219. package/dist/core/v2/overrides.d.ts +7 -0
  220. package/dist/core/v2/overrides.js +37 -0
  221. package/dist/core/v2/parser.d.ts +21 -0
  222. package/dist/core/v2/parser.js +213 -0
  223. package/dist/core/v2/types.d.ts +67 -0
  224. package/dist/core/v2/types.js +1 -0
  225. package/dist/core/v2/verdict.d.ts +2 -0
  226. package/dist/core/v2/verdict.js +699 -0
  227. package/dist/corpus/evaluate.d.ts +24 -0
  228. package/dist/corpus/evaluate.js +69 -0
  229. package/dist/defaults.d.ts +18 -0
  230. package/dist/defaults.js +155 -0
  231. package/dist/egress-daemon.d.ts +1 -0
  232. package/dist/egress-daemon.js +52 -0
  233. package/dist/index.d.ts +17 -0
  234. package/dist/index.js +15 -0
  235. package/dist/installer/bootstrap.d.ts +5 -0
  236. package/dist/installer/bootstrap.js +61 -0
  237. package/dist/installer/runtime-artifacts.d.ts +3 -0
  238. package/dist/installer/runtime-artifacts.js +23 -0
  239. package/dist/installer/scope-config.d.ts +8 -0
  240. package/dist/installer/scope-config.js +25 -0
  241. package/dist/installer.d.ts +22 -0
  242. package/dist/installer.js +169 -0
  243. package/dist/node-resolution.d.ts +8 -0
  244. package/dist/node-resolution.js +237 -0
  245. package/dist/operational-insights.d.ts +19 -0
  246. package/dist/operational-insights.js +24 -0
  247. package/dist/presets.d.ts +4 -0
  248. package/dist/presets.js +95 -0
  249. package/dist/services/egress-service.d.ts +57 -0
  250. package/dist/services/egress-service.js +334 -0
  251. package/dist/services/sandbox-service.d.ts +38 -0
  252. package/dist/services/sandbox-service.js +95 -0
  253. package/dist/templates.d.ts +7 -0
  254. package/dist/templates.js +56 -0
  255. package/dist/types.d.ts +230 -0
  256. package/dist/types.js +1 -0
  257. package/dist/version.d.ts +1 -0
  258. package/dist/version.js +1 -0
  259. package/package.json +65 -0
  260. package/skills/belay/SKILL.md +52 -0
  261. package/skills/belay/belay-approve.md +7 -0
  262. package/skills/belay/belay-explain.md +11 -0
  263. package/skills/belay/belay-recover.md +13 -0
  264. package/skills/belay/belay-report.md +7 -0
  265. package/skills/belay/belay-status.md +9 -0
  266. package/skills/belay/belay-why.md +11 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,268 @@
1
+ # Belay
2
+
3
+ [![npm version](https://img.shields.io/npm/v/@guilz-dev/belay)](https://www.npmjs.com/package/@guilz-dev/belay)
4
+ [![CI](https://github.com/guilz-dev/belay/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/guilz-dev/belay/actions/workflows/ci.yml)
5
+ [![License](https://img.shields.io/badge/License-MIT-blue.svg)](https://opensource.org/licenses/MIT)
6
+
7
+ **A safety gate for coding agents that stops only the actions you can't undo.**
8
+
9
+ [Documentation (日本語)](./docs/README.ja.md)
10
+
11
+ `@guilz-dev/belay` hooks into agent runtimes (Cursor, Claude Code) and inspects
12
+ each shell command, subagent launch, and file mutation *before* it runs. Most
13
+ actions pass through untouched. Only the irreversible-and-catastrophic ones are
14
+ held back for one-shot human approval — and every decision is written to an
15
+ audit log.
16
+
17
+ <p align="center">
18
+ <img src="./agent-belay-logo.png" alt="Belay logo" width="480">
19
+ </p>
20
+
21
+ > **0.0.x early release** — APIs and behavior may change. Cursor and Claude Code
22
+ > are the supported adapters; Codex is experimental.
23
+
24
+ ## Why
25
+
26
+ Static denylists don't work for agents. The same command (`rm`, `curl`, a
27
+ deploy script) can be harmless in one context and catastrophic in another, and a
28
+ hand-maintained "never run this" list is always out of date and easy to work
29
+ around.
30
+
31
+ Belay moves the decision away from command names. For every gated action it
32
+ forms its own judgment based on:
33
+
34
+ - **reversibility** — can this be undone?
35
+ - **external effects** — does it reach outside the machine?
36
+ - **blast radius** — how much could it affect?
37
+ - **confidence** — how sure are we?
38
+
39
+ When the action looks safe and local, it runs. When it looks irreversible,
40
+ externally destructive, or ambiguous, Belay falls back to explicit approval and
41
+ audit instead of guessing.
42
+
43
+ ## Quick start
44
+
45
+ ```bash
46
+ # Interactive setup (prompts for adapter, scope, skill, mode)
47
+ npx @guilz-dev/belay init-wizard
48
+
49
+ # Or non-interactive
50
+ npx @guilz-dev/belay init --adapter claude # Claude Code
51
+ npx @guilz-dev/belay init # Cursor (default)
52
+ ```
53
+
54
+ After install, verify the floor is healthy:
55
+
56
+ ```bash
57
+ npx @guilz-dev/belay doctor
58
+ npx @guilz-dev/belay status
59
+ ```
60
+
61
+ Fresh installs default to **fail-closed** shell policy: unknown or unparseable
62
+ shell commands are denied until approved. Use `belay explain` to inspect a
63
+ verdict and `overrides.allow` to whitelist commands you trust.
64
+
65
+ ## How it works
66
+
67
+ Belay registers hooks on the host runtime (`.cursor/hooks.json` or
68
+ `.claude/settings.json`) and gates shell execution, subagent launches, and file
69
+ mutations through one shared classifier. It always forms its own judgment — it
70
+ does not trust an assessment supplied by the agent.
71
+
72
+ Every gated action gets one of three verdicts:
73
+
74
+ | Verdict | Meaning |
75
+ |---------|---------|
76
+ | `allow` | Safe and read-only — runs without intervention |
77
+ | `allow_flagged` | Local mutation or unknown-but-local effect — runs, but recorded for audit |
78
+ | `deny_pending_approval` | Irreversible, externally destructive, or ambiguous — blocked, issues an approval ID |
79
+
80
+ When an action is denied, approve the **next matching action once** by sending:
81
+
82
+ ```text
83
+ /belay-approve <approval-id>
84
+ ```
85
+
86
+ Approvals are one-shot and expire after 15 minutes by default. Every decision is
87
+ written to `.cursor/belay/audit.ndjson` (or `.claude/belay/audit.ndjson`).
88
+
89
+ In **audit mode** (`mode: "audit"`), would-be denials are recorded
90
+ (`wouldBlock: true`) but execution still continues, and no approval IDs are
91
+ created. This is the recommended way to dogfood before enforcing.
92
+
93
+ ## Layers
94
+
95
+ Belay is a layered hook gate, not a static denylist. Higher layers are opt-in.
96
+
97
+ | Layer | Role | Enabled by |
98
+ |-------|------|------------|
99
+ | **L1** Containment | Egress proxy, sandbox capability broker | `egress` / `sandbox` config |
100
+ | **L2** Observation | Transactional git-worktree diff | `policy.transactional` |
101
+ | **L3** Prediction | Policy rules + command heuristics | default |
102
+ | **L4** Approval | Human one-shot / scoped approvals | default |
103
+
104
+ - L3 command lists are **not security boundaries** by themselves — see
105
+ [docs/ops/semver-policy.md](./docs/ops/semver-policy.md) and
106
+ [docs/guarantee-table.md](./docs/guarantee-table.md).
107
+ - Adversarial resistance requires the full L1 stack:
108
+ `belay init --preset l1-full-recommended`, verified with `belay sandbox status`.
109
+
110
+ ## Install options
111
+
112
+ ```bash
113
+ npx @guilz-dev/belay init --with-skill # also install skill + slash commands
114
+ npx @guilz-dev/belay init --scope global # hooks/runtime under ~/.cursor/ etc.
115
+ npx @guilz-dev/belay init --dogfood # audit mode, fail-closed classification
116
+ npx @guilz-dev/belay upgrade # refresh hooks/runtime, migrate config
117
+ ```
118
+
119
+ **Install scope.** `--scope project` (default) writes artifacts under
120
+ `.cursor/` (or `.claude/`, `.codex/`). `--scope global` installs hooks, runtime,
121
+ and skill under `~/.cursor/`, so the gate is user-wide while `belay.config.json`,
122
+ approvals, and audit stay repo-local.
123
+
124
+ **Skill-only.** The skill is just a UX layer (slash commands + guidance) and does
125
+ **not** enable gating on its own:
126
+
127
+ ```bash
128
+ npx skills add guilz-dev/belay --skill belay -a cursor -y
129
+ ```
130
+
131
+ Runtime enforcement still requires `belay init` in the target repository.
132
+
133
+ ## Dogfood → enforce
134
+
135
+ ```bash
136
+ npx @guilz-dev/belay dogfood # mode: audit, unknownLocalEffect: deny
137
+ # ...run normal agent work...
138
+ npx @guilz-dev/belay metrics # review what would have been blocked
139
+ npx @guilz-dev/belay status # check readiness
140
+ # tune overrides.allow with `belay explain`, then:
141
+ npx @guilz-dev/belay dogfood --enforce
142
+ ```
143
+
144
+ ## Configuration
145
+
146
+ `belay.config.json` uses `version: 3`. v1/v2 configs migrate automatically on
147
+ load.
148
+
149
+ ```json
150
+ {
151
+ "version": 3,
152
+ "mode": "enforce",
153
+ "gates": {
154
+ "shell": true,
155
+ "subagent": true,
156
+ "fileMutation": true,
157
+ "toolShell": true
158
+ },
159
+ "classifier": {
160
+ "strictChains": true,
161
+ "sensitivePaths": [".env", ".env.*", "**/credentials/**"]
162
+ },
163
+ "policy": {
164
+ "unknownLocalEffect": "allow_flagged"
165
+ },
166
+ "overrides": {
167
+ "allow": ["pnpm release:staging"],
168
+ "external": ["./scripts/release.sh"]
169
+ },
170
+ "redaction": {
171
+ "maskApprovalIds": true,
172
+ "maskBearerTokens": true,
173
+ "maskAuthHeaders": true,
174
+ "maskKeyValueSecrets": true,
175
+ "maskHighEntropyStrings": false
176
+ },
177
+ "controlPlane": {
178
+ "enabled": false,
179
+ "configDir": null
180
+ },
181
+ "audit": {
182
+ "logPath": ".cursor/belay/audit.ndjson",
183
+ "includeAssessment": true
184
+ }
185
+ }
186
+ ```
187
+
188
+ Notable settings:
189
+
190
+ - **`policy.unknownLocalEffect: "deny"`** — fail-closed classification for
191
+ unrecognized local commands.
192
+ - **`classifier.strictChains: true`** (default) — scans every `&&`, `|`, and `;`
193
+ segment and keeps the strictest verdict. Override lists match exact command or
194
+ segment keys only.
195
+ - **`controlPlane.enabled: true`** — stores approval state under
196
+ `~/.config/belay/` (or `XDG_CONFIG_HOME/belay`), shared across repos for the
197
+ current OS user. `upgrade` migrates repo-local approvals in; disabling merges
198
+ them back. File-mutation tools and shell redirects cannot write control-plane
199
+ paths while it is enabled.
200
+ - **Cloud judge** — for `judge.provider: "openai-compatible"`, set
201
+ `judge.endpoint` and provide `BELAY_JUDGE_API_KEY` (or `OPENAI_API_KEY`), or
202
+ opt in with
203
+ `belay init --judge-provider openai-compatible --judge-endpoint <url> --accept-cloud-judge`.
204
+ Fresh installs default to local Ollama (`local-ollama`).
205
+
206
+ ## Command reference
207
+
208
+ ```bash
209
+ belay init [--adapter cursor|claude|codex] [--scope project|global]
210
+ [--preset strict|standard|audit-first|l1-full-recommended]
211
+ [--with-skill] [--dogfood]
212
+ belay init-wizard # interactive install
213
+ belay upgrade # refresh hooks + runtime, migrate config
214
+ belay dogfood [--enforce] # toggle audit / enforce mode
215
+ belay doctor [--fix] # check (and repair) floor health
216
+ belay status # show install scope / skill-only state
217
+ belay metrics # would-block / verdict summary
218
+ belay report # audit log report
219
+ belay recover [--command "rm important.ts"] # find recovery candidates
220
+ belay explain -- <shell-command> # inspect a verdict
221
+ belay explain --kind subagent -- "deploy to production"
222
+ belay explain --kind tool --tool Write -- .env
223
+ belay egress <start|stop|status|env>
224
+ belay sandbox status
225
+ belay approve <approval-id> [--scope once|domain|path]
226
+ belay revoke <approval-id>
227
+ ```
228
+
229
+ ## Coexisting with existing hooks
230
+
231
+ Belay is designed to run alongside your other repo-local hooks:
232
+
233
+ - Gate hooks are **prepended** so they run before existing hooks for the same event.
234
+ - Audit hooks are **appended** so they observe the final flow.
235
+ - Existing non-Belay hook entries are preserved in order.
236
+
237
+ If another hook also denies an event, the host runtime still blocks it — Belay
238
+ does not suppress other repo policies.
239
+
240
+ ## Git hygiene
241
+
242
+ Belay state files are local runtime artifacts and should usually stay out of git:
243
+
244
+ ```gitignore
245
+ .cursor/belay/
246
+ .cursor/belay.config.json
247
+ .cursor/hooks/belay-*
248
+ .cursor/skills/belay/
249
+ .cursor/commands/belay-approve.md
250
+ ```
251
+
252
+ ## Library exports
253
+
254
+ The package exposes a testable core for classification and config migration:
255
+
256
+ ```ts
257
+ import { classifyShell, DEFAULT_CONFIG_V3, mergeConfig } from 'belay'
258
+
259
+ const result = await classifyShell('git status', process.cwd(), process.cwd(), mergeConfig({}))
260
+ ```
261
+
262
+ See `belay/core` for lower-level exports.
263
+
264
+ ## Roadmap & history
265
+
266
+ Release notes and the version-by-version roadmap live in
267
+ [CHANGELOG.md](./CHANGELOG.md) and [docs/ROADMAP.md](./docs/ROADMAP.md).
268
+ Japanese documentation index: [docs/README.ja.md](./docs/README.ja.md).
Binary file
@@ -0,0 +1,7 @@
1
+ import type { BelayAdapter } from '../types.js';
2
+ export declare const claudeAdapter: BelayAdapter;
3
+ export declare function claudePaths(repoRoot: string): {
4
+ config: string;
5
+ hooks: string;
6
+ runtime: string;
7
+ };
@@ -0,0 +1,114 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { mkdir, readFile, writeFile } from 'node:fs/promises';
3
+ import path from 'node:path';
4
+ import { doctorProject } from '../../commands/doctor.js';
5
+ import { mergeAndWriteConfig } from '../../config-io.js';
6
+ import { runtimeIntegrityFiles, writeIntegrityManifest } from '../../core/integrity.js';
7
+ import { bootstrapStateFiles, writeSkillArtifacts } from '../../installer/bootstrap.js';
8
+ import { writeRuntimeArtifacts } from '../../installer/runtime-artifacts.js';
9
+ import { applyInstallScope, resolveOperationScope } from '../../installer/scope-config.js';
10
+ import { claudeLayout } from '../layouts/claude.js';
11
+ import { resolveScopedPaths } from '../layouts/scope.js';
12
+ import { getClaudeManagedHookGroups } from './hooks.js';
13
+ function hookCommandMatches(existing, expectedCommand) {
14
+ if (!existing || typeof existing !== 'object') {
15
+ return false;
16
+ }
17
+ const record = existing;
18
+ return (Array.isArray(record.hooks) &&
19
+ record.hooks.some((hook) => hook.type === 'command' && hook.command === expectedCommand));
20
+ }
21
+ function mergeClaudeHookGroup(current, expected) {
22
+ const entries = Array.isArray(current) ? [...current] : [];
23
+ const expectedCommand = expected.hooks[0]?.command;
24
+ const filtered = entries.filter((entry) => {
25
+ if (!expectedCommand) {
26
+ return true;
27
+ }
28
+ return !hookCommandMatches(entry, expectedCommand);
29
+ });
30
+ return [expected, ...filtered];
31
+ }
32
+ async function loadClaudeSettings(settingsPath) {
33
+ if (!existsSync(settingsPath)) {
34
+ return {};
35
+ }
36
+ const raw = await readFile(settingsPath, 'utf8');
37
+ return JSON.parse(raw);
38
+ }
39
+ function mergeClaudeSettings(current, platform, hooksDir, repoRoot) {
40
+ const managed = getClaudeManagedHookGroups(platform, hooksDir, repoRoot);
41
+ const hooks = { ...(current.hooks ?? {}) };
42
+ for (const [event, groups] of Object.entries(managed)) {
43
+ let eventHooks = Array.isArray(hooks[event]) ? [...hooks[event]] : [];
44
+ for (const group of groups) {
45
+ eventHooks = mergeClaudeHookGroup(eventHooks, group);
46
+ }
47
+ hooks[event] = eventHooks;
48
+ }
49
+ return {
50
+ ...current,
51
+ hooks,
52
+ };
53
+ }
54
+ async function installClaudeBase(repoRoot, options) {
55
+ const scope = await resolveOperationScope(repoRoot, 'claude', options);
56
+ const paths = resolveScopedPaths(claudeLayout, scope, repoRoot);
57
+ const settingsPath = paths.hooksSettingsPath;
58
+ const settings = mergeClaudeSettings(await loadClaudeSettings(settingsPath), process.platform, paths.hooksDir, repoRoot);
59
+ const config = await mergeAndWriteConfig(repoRoot, 'claude');
60
+ await applyInstallScope(repoRoot, 'claude', scope, config);
61
+ await writeRuntimeArtifacts('claude', paths);
62
+ await bootstrapStateFiles(repoRoot, config, paths);
63
+ if (options.withSkill) {
64
+ await writeSkillArtifacts('claude', paths);
65
+ }
66
+ await mkdir(path.dirname(settingsPath), { recursive: true });
67
+ await writeFile(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, 'utf8');
68
+ await writeIntegrityManifest(repoRoot, claudeLayout, runtimeIntegrityFiles(claudeLayout, paths));
69
+ }
70
+ export const claudeAdapter = {
71
+ name: 'claude',
72
+ layout: claudeLayout,
73
+ async install(repoRoot, options = {}) {
74
+ await installClaudeBase(repoRoot, options);
75
+ return { repoRoot, withSkill: options.withSkill === true };
76
+ },
77
+ async upgrade(repoRoot, options = {}) {
78
+ const scope = await resolveOperationScope(repoRoot, 'claude', options);
79
+ const paths = resolveScopedPaths(claudeLayout, scope, repoRoot);
80
+ const config = await mergeAndWriteConfig(repoRoot, 'claude');
81
+ await applyInstallScope(repoRoot, 'claude', scope, config);
82
+ await writeRuntimeArtifacts('claude', paths);
83
+ const settingsPath = paths.hooksSettingsPath;
84
+ const settings = mergeClaudeSettings(await loadClaudeSettings(settingsPath), process.platform, paths.hooksDir, repoRoot);
85
+ await mkdir(path.dirname(settingsPath), { recursive: true });
86
+ await writeFile(settingsPath, `${JSON.stringify(settings, null, 2)}\n`, 'utf8');
87
+ if (options.withSkill) {
88
+ await writeSkillArtifacts('claude', paths);
89
+ }
90
+ await writeIntegrityManifest(repoRoot, claudeLayout, runtimeIntegrityFiles(claudeLayout, paths));
91
+ return { repoRoot };
92
+ },
93
+ async doctor(options = {}) {
94
+ return doctorProject({ ...options, adapter: 'claude' });
95
+ },
96
+ hookEvents() {
97
+ return getClaudeManagedHookGroups(process.platform, claudeLayout.hooksDir(process.cwd()), process.cwd()).PreToolUse.map((group) => ({
98
+ event: 'PreToolUse',
99
+ definition: {
100
+ command: group.hooks[0]?.command ?? '',
101
+ placement: 'prepend',
102
+ matcher: group.matcher,
103
+ },
104
+ }));
105
+ },
106
+ };
107
+ export function claudePaths(repoRoot) {
108
+ const resolved = path.resolve(repoRoot);
109
+ return {
110
+ config: claudeLayout.configPath(resolved),
111
+ hooks: claudeLayout.hooksSettingsPath(resolved),
112
+ runtime: path.join(claudeLayout.runtimeDir(resolved), 'core.mjs'),
113
+ };
114
+ }
@@ -0,0 +1,13 @@
1
+ import type { ManagedHookDefinition } from '../../defaults.js';
2
+ export interface ClaudeHookGroup {
3
+ matcher?: string;
4
+ hooks: Array<{
5
+ type: 'command';
6
+ command: string;
7
+ }>;
8
+ }
9
+ export declare function getClaudeManagedHookGroups(platform: NodeJS.Platform, hooksDir: string, repoRoot: string): Record<string, ClaudeHookGroup[]>;
10
+ export declare function getClaudeManagedHookEntries(platform?: NodeJS.Platform, hooksDir?: string, repoRoot?: string): Array<{
11
+ event: string;
12
+ definition: ManagedHookDefinition;
13
+ }>;
@@ -0,0 +1,49 @@
1
+ import path from 'node:path';
2
+ import { buildRunnerInvocation } from '../layouts/scope.js';
3
+ export function getClaudeManagedHookGroups(platform, hooksDir, repoRoot) {
4
+ const runner = (hookName, ...args) => buildRunnerInvocation(platform, hooksDir, repoRoot, hookName, ...args);
5
+ const toolGate = runner('belay-tool-gate', 'PreToolUse');
6
+ const approvalGate = runner('belay-before-submit');
7
+ const auditHook = runner('belay-audit', 'PostToolUse');
8
+ return {
9
+ PreToolUse: [
10
+ {
11
+ matcher: '*',
12
+ hooks: [{ type: 'command', command: toolGate }],
13
+ },
14
+ ],
15
+ UserPromptSubmit: [
16
+ {
17
+ hooks: [{ type: 'command', command: approvalGate }],
18
+ },
19
+ ],
20
+ PostToolUse: [
21
+ {
22
+ hooks: [{ type: 'command', command: auditHook }],
23
+ },
24
+ ],
25
+ };
26
+ }
27
+ export function getClaudeManagedHookEntries(platform = process.platform, hooksDir, repoRoot) {
28
+ const resolvedRepo = path.resolve(repoRoot ?? process.cwd());
29
+ const resolvedHooksDir = hooksDir ?? path.join(resolvedRepo, '.claude', 'hooks');
30
+ const groups = getClaudeManagedHookGroups(platform, resolvedHooksDir, resolvedRepo);
31
+ const entries = [];
32
+ for (const [event, groupList] of Object.entries(groups)) {
33
+ for (const group of groupList) {
34
+ const command = group.hooks[0]?.command;
35
+ if (!command) {
36
+ continue;
37
+ }
38
+ entries.push({
39
+ event,
40
+ definition: {
41
+ command,
42
+ placement: 'prepend',
43
+ matcher: group.matcher,
44
+ },
45
+ });
46
+ }
47
+ }
48
+ return entries;
49
+ }
@@ -0,0 +1,4 @@
1
+ export declare function runBeforeSubmitPromptHook(): Promise<void>;
2
+ export declare function runShellGateHook(): Promise<void>;
3
+ export declare function runToolGateHook(_eventName: string): Promise<void>;
4
+ export declare function runAuditHook(eventName: string): Promise<void>;