@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.
- package/LICENSE +21 -0
- package/README.md +268 -0
- package/agent-belay-logo.png +0 -0
- package/dist/adapters/claude/adapter.d.ts +7 -0
- package/dist/adapters/claude/adapter.js +114 -0
- package/dist/adapters/claude/hooks.d.ts +13 -0
- package/dist/adapters/claude/hooks.js +49 -0
- package/dist/adapters/claude/runtime-entry.d.ts +4 -0
- package/dist/adapters/claude/runtime-entry.js +260 -0
- package/dist/adapters/codex/adapter.d.ts +7 -0
- package/dist/adapters/codex/adapter.js +73 -0
- package/dist/adapters/codex/hooks.d.ts +21 -0
- package/dist/adapters/codex/hooks.js +78 -0
- package/dist/adapters/codex/runtime-entry.d.ts +4 -0
- package/dist/adapters/codex/runtime-entry.js +237 -0
- package/dist/adapters/cursor/adapter.d.ts +7 -0
- package/dist/adapters/cursor/adapter.js +29 -0
- package/dist/adapters/cursor/hooks.d.ts +2 -0
- package/dist/adapters/cursor/hooks.js +26 -0
- package/dist/adapters/cursor/runtime-entry.d.ts +4 -0
- package/dist/adapters/cursor/runtime-entry.js +143 -0
- package/dist/adapters/layouts/claude.d.ts +2 -0
- package/dist/adapters/layouts/claude.js +40 -0
- package/dist/adapters/layouts/codex.d.ts +2 -0
- package/dist/adapters/layouts/codex.js +43 -0
- package/dist/adapters/layouts/cursor.d.ts +2 -0
- package/dist/adapters/layouts/cursor.js +40 -0
- package/dist/adapters/layouts/index.d.ts +7 -0
- package/dist/adapters/layouts/index.js +23 -0
- package/dist/adapters/layouts/protected-paths.d.ts +3 -0
- package/dist/adapters/layouts/protected-paths.js +15 -0
- package/dist/adapters/layouts/scope.d.ts +19 -0
- package/dist/adapters/layouts/scope.js +86 -0
- package/dist/adapters/layouts/types.d.ts +14 -0
- package/dist/adapters/layouts/types.js +1 -0
- package/dist/adapters/registry.d.ts +4 -0
- package/dist/adapters/registry.js +14 -0
- package/dist/adapters/shared/gate-runtime.d.ts +51 -0
- package/dist/adapters/shared/gate-runtime.js +518 -0
- package/dist/adapters/shared/repo-root.d.ts +2 -0
- package/dist/adapters/shared/repo-root.js +17 -0
- package/dist/adapters/types.d.ts +19 -0
- package/dist/adapters/types.js +1 -0
- package/dist/branding.d.ts +3 -0
- package/dist/branding.js +3 -0
- package/dist/bundle/claude-runtime.mjs +5323 -0
- package/dist/bundle/codex-runtime.mjs +5310 -0
- package/dist/bundle/cursor-runtime.mjs +5208 -0
- package/dist/cleanup-orphans.d.ts +7 -0
- package/dist/cleanup-orphans.js +59 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +631 -0
- package/dist/commands/approve.d.ts +14 -0
- package/dist/commands/approve.js +65 -0
- package/dist/commands/audit.d.ts +59 -0
- package/dist/commands/audit.js +132 -0
- package/dist/commands/classify-for-report.d.ts +9 -0
- package/dist/commands/classify-for-report.js +85 -0
- package/dist/commands/doctor.d.ts +3 -0
- package/dist/commands/doctor.js +366 -0
- package/dist/commands/dogfood.d.ts +5 -0
- package/dist/commands/dogfood.js +71 -0
- package/dist/commands/explain.d.ts +3 -0
- package/dist/commands/explain.js +133 -0
- package/dist/commands/health-snapshot.d.ts +2 -0
- package/dist/commands/health-snapshot.js +166 -0
- package/dist/commands/init-wizard.d.ts +16 -0
- package/dist/commands/init-wizard.js +50 -0
- package/dist/commands/metrics.d.ts +7 -0
- package/dist/commands/metrics.js +89 -0
- package/dist/commands/recover.d.ts +3 -0
- package/dist/commands/recover.js +105 -0
- package/dist/commands/report.d.ts +3 -0
- package/dist/commands/report.js +65 -0
- package/dist/commands/revoke.d.ts +5 -0
- package/dist/commands/revoke.js +22 -0
- package/dist/commands/simulate.d.ts +14 -0
- package/dist/commands/simulate.js +55 -0
- package/dist/commands/status.d.ts +5 -0
- package/dist/commands/status.js +107 -0
- package/dist/config-io.d.ts +23 -0
- package/dist/config-io.js +180 -0
- package/dist/conformance/guarantee-table.d.ts +14 -0
- package/dist/conformance/guarantee-table.js +95 -0
- package/dist/conformance/types.d.ts +6 -0
- package/dist/conformance/types.js +1 -0
- package/dist/core/approval-service.d.ts +26 -0
- package/dist/core/approval-service.js +41 -0
- package/dist/core/approval-token.d.ts +11 -0
- package/dist/core/approval-token.js +61 -0
- package/dist/core/approval.d.ts +19 -0
- package/dist/core/approval.js +58 -0
- package/dist/core/audit-analysis.d.ts +10 -0
- package/dist/core/audit-analysis.js +147 -0
- package/dist/core/audit-metrics.d.ts +51 -0
- package/dist/core/audit-metrics.js +155 -0
- package/dist/core/audit-query.d.ts +11 -0
- package/dist/core/audit-query.js +142 -0
- package/dist/core/audit-summary.d.ts +33 -0
- package/dist/core/audit-summary.js +111 -0
- package/dist/core/audit-types.d.ts +65 -0
- package/dist/core/audit-types.js +2 -0
- package/dist/core/capability/allowlist.d.ts +10 -0
- package/dist/core/capability/allowlist.js +53 -0
- package/dist/core/capability/broker.d.ts +17 -0
- package/dist/core/capability/broker.js +29 -0
- package/dist/core/capability/index.d.ts +5 -0
- package/dist/core/capability/index.js +4 -0
- package/dist/core/capability/paths.d.ts +1 -0
- package/dist/core/capability/paths.js +20 -0
- package/dist/core/capability/reasons.d.ts +2 -0
- package/dist/core/capability/reasons.js +4 -0
- package/dist/core/capability/types.d.ts +10 -0
- package/dist/core/capability/types.js +1 -0
- package/dist/core/capability-approval.d.ts +28 -0
- package/dist/core/capability-approval.js +43 -0
- package/dist/core/classify-subagent.d.ts +2 -0
- package/dist/core/classify-subagent.js +69 -0
- package/dist/core/classify-tool.d.ts +3 -0
- package/dist/core/classify-tool.js +311 -0
- package/dist/core/config-layers.d.ts +23 -0
- package/dist/core/config-layers.js +59 -0
- package/dist/core/config.d.ts +219 -0
- package/dist/core/config.js +720 -0
- package/dist/core/control-plane-isolation.d.ts +10 -0
- package/dist/core/control-plane-isolation.js +83 -0
- package/dist/core/custom-command-match.d.ts +2 -0
- package/dist/core/custom-command-match.js +8 -0
- package/dist/core/egress/allowlist.d.ts +7 -0
- package/dist/core/egress/allowlist.js +33 -0
- package/dist/core/egress/env.d.ts +3 -0
- package/dist/core/egress/env.js +17 -0
- package/dist/core/egress/fingerprint.d.ts +3 -0
- package/dist/core/egress/fingerprint.js +35 -0
- package/dist/core/egress/policy.d.ts +8 -0
- package/dist/core/egress/policy.js +47 -0
- package/dist/core/egress/proxy-server.d.ts +21 -0
- package/dist/core/egress/proxy-server.js +263 -0
- package/dist/core/egress/types.d.ts +25 -0
- package/dist/core/egress/types.js +1 -0
- package/dist/core/egress-approval.d.ts +48 -0
- package/dist/core/egress-approval.js +129 -0
- package/dist/core/fingerprint.d.ts +6 -0
- package/dist/core/fingerprint.js +24 -0
- package/dist/core/gate-contract.d.ts +48 -0
- package/dist/core/gate-contract.js +50 -0
- package/dist/core/gate-engine.d.ts +20 -0
- package/dist/core/gate-engine.js +172 -0
- package/dist/core/glob.d.ts +1 -0
- package/dist/core/glob.js +39 -0
- package/dist/core/index.d.ts +19 -0
- package/dist/core/index.js +15 -0
- package/dist/core/integrity.d.ts +15 -0
- package/dist/core/integrity.js +68 -0
- package/dist/core/judge-api-key.d.ts +4 -0
- package/dist/core/judge-api-key.js +11 -0
- package/dist/core/judge-config.d.ts +29 -0
- package/dist/core/judge-config.js +85 -0
- package/dist/core/judge-doctor.d.ts +7 -0
- package/dist/core/judge-doctor.js +124 -0
- package/dist/core/judgment.d.ts +6 -0
- package/dist/core/judgment.js +38 -0
- package/dist/core/notify.d.ts +13 -0
- package/dist/core/notify.js +44 -0
- package/dist/core/path-utils.d.ts +12 -0
- package/dist/core/path-utils.js +107 -0
- package/dist/core/reclassify.d.ts +15 -0
- package/dist/core/reclassify.js +82 -0
- package/dist/core/recover-advice.d.ts +30 -0
- package/dist/core/recover-advice.js +177 -0
- package/dist/core/recover-git-probe.d.ts +8 -0
- package/dist/core/recover-git-probe.js +50 -0
- package/dist/core/recover-select.d.ts +10 -0
- package/dist/core/recover-select.js +60 -0
- package/dist/core/scrub.d.ts +3 -0
- package/dist/core/scrub.js +87 -0
- package/dist/core/shell-substitution.d.ts +6 -0
- package/dist/core/shell-substitution.js +130 -0
- package/dist/core/shell-tokenizer.d.ts +5 -0
- package/dist/core/shell-tokenizer.js +129 -0
- package/dist/core/shell-unparseable.d.ts +4 -0
- package/dist/core/shell-unparseable.js +96 -0
- package/dist/core/transactional/diff-evaluator.d.ts +2 -0
- package/dist/core/transactional/diff-evaluator.js +84 -0
- package/dist/core/transactional/eligibility.d.ts +4 -0
- package/dist/core/transactional/eligibility.js +44 -0
- package/dist/core/transactional/git-worktree.d.ts +13 -0
- package/dist/core/transactional/git-worktree.js +189 -0
- package/dist/core/transactional/index.d.ts +5 -0
- package/dist/core/transactional/index.js +4 -0
- package/dist/core/transactional/reasons.d.ts +4 -0
- package/dist/core/transactional/reasons.js +8 -0
- package/dist/core/transactional/runner.d.ts +2 -0
- package/dist/core/transactional/runner.js +113 -0
- package/dist/core/transactional/types.d.ts +46 -0
- package/dist/core/transactional/types.js +1 -0
- package/dist/core/types.d.ts +90 -0
- package/dist/core/types.js +1 -0
- package/dist/core/v2/adapter.d.ts +14 -0
- package/dist/core/v2/adapter.js +118 -0
- package/dist/core/v2/containment.d.ts +19 -0
- package/dist/core/v2/containment.js +91 -0
- package/dist/core/v2/egress-classify.d.ts +7 -0
- package/dist/core/v2/egress-classify.js +216 -0
- package/dist/core/v2/fingerprint.d.ts +1 -0
- package/dist/core/v2/fingerprint.js +4 -0
- package/dist/core/v2/index.d.ts +12 -0
- package/dist/core/v2/index.js +10 -0
- package/dist/core/v2/judge-audit.d.ts +2 -0
- package/dist/core/v2/judge-audit.js +15 -0
- package/dist/core/v2/judge-factory.d.ts +25 -0
- package/dist/core/v2/judge-factory.js +75 -0
- package/dist/core/v2/judge-outbound.d.ts +12 -0
- package/dist/core/v2/judge-outbound.js +73 -0
- package/dist/core/v2/judge.d.ts +47 -0
- package/dist/core/v2/judge.js +264 -0
- package/dist/core/v2/launcher-resolve.d.ts +12 -0
- package/dist/core/v2/launcher-resolve.js +190 -0
- package/dist/core/v2/overrides.d.ts +7 -0
- package/dist/core/v2/overrides.js +37 -0
- package/dist/core/v2/parser.d.ts +21 -0
- package/dist/core/v2/parser.js +213 -0
- package/dist/core/v2/types.d.ts +67 -0
- package/dist/core/v2/types.js +1 -0
- package/dist/core/v2/verdict.d.ts +2 -0
- package/dist/core/v2/verdict.js +699 -0
- package/dist/corpus/evaluate.d.ts +24 -0
- package/dist/corpus/evaluate.js +69 -0
- package/dist/defaults.d.ts +18 -0
- package/dist/defaults.js +155 -0
- package/dist/egress-daemon.d.ts +1 -0
- package/dist/egress-daemon.js +52 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +15 -0
- package/dist/installer/bootstrap.d.ts +5 -0
- package/dist/installer/bootstrap.js +61 -0
- package/dist/installer/runtime-artifacts.d.ts +3 -0
- package/dist/installer/runtime-artifacts.js +23 -0
- package/dist/installer/scope-config.d.ts +8 -0
- package/dist/installer/scope-config.js +25 -0
- package/dist/installer.d.ts +22 -0
- package/dist/installer.js +169 -0
- package/dist/node-resolution.d.ts +8 -0
- package/dist/node-resolution.js +237 -0
- package/dist/operational-insights.d.ts +19 -0
- package/dist/operational-insights.js +24 -0
- package/dist/presets.d.ts +4 -0
- package/dist/presets.js +95 -0
- package/dist/services/egress-service.d.ts +57 -0
- package/dist/services/egress-service.js +334 -0
- package/dist/services/sandbox-service.d.ts +38 -0
- package/dist/services/sandbox-service.js +95 -0
- package/dist/templates.d.ts +7 -0
- package/dist/templates.js +56 -0
- package/dist/types.d.ts +230 -0
- package/dist/types.js +1 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +1 -0
- package/package.json +65 -0
- package/skills/belay/SKILL.md +52 -0
- package/skills/belay/belay-approve.md +7 -0
- package/skills/belay/belay-explain.md +11 -0
- package/skills/belay/belay-recover.md +13 -0
- package/skills/belay/belay-report.md +7 -0
- package/skills/belay/belay-status.md +9 -0
- 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
|
+
[](https://www.npmjs.com/package/@guilz-dev/belay)
|
|
4
|
+
[](https://github.com/guilz-dev/belay/actions/workflows/ci.yml)
|
|
5
|
+
[](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,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>;
|