@scopebond/hook 0.4.0 → 0.6.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 +74 -22
- package/dist/cli.js +124 -31
- package/dist/cli.js.map +1 -1
- package/dist/cloud.d.ts.map +1 -1
- package/dist/cloud.js +2 -1
- package/dist/cloud.js.map +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -4
- package/dist/index.js.map +1 -1
- package/dist/init.d.ts +2 -2
- package/dist/init.d.ts.map +1 -1
- package/dist/init.js +19 -11
- package/dist/init.js.map +1 -1
- package/dist/install.d.ts +21 -2
- package/dist/install.d.ts.map +1 -1
- package/dist/install.js +101 -24
- package/dist/install.js.map +1 -1
- package/dist/map.d.ts +10 -3
- package/dist/map.d.ts.map +1 -1
- package/dist/map.js +799 -54
- package/dist/map.js.map +1 -1
- package/dist/minimize.d.ts +6 -3
- package/dist/minimize.d.ts.map +1 -1
- package/dist/minimize.js +24 -4
- package/dist/minimize.js.map +1 -1
- package/dist/runtime.d.ts +4 -0
- package/dist/runtime.d.ts.map +1 -1
- package/dist/runtime.js +75 -9
- package/dist/runtime.js.map +1 -1
- package/dist/shell.d.ts +53 -4
- package/dist/shell.d.ts.map +1 -1
- package/dist/shell.js +737 -81
- package/dist/shell.js.map +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.d.ts.map +1 -1
- package/dist/version.js +1 -1
- package/dist/version.js.map +1 -1
- package/package.json +6 -5
package/README.md
CHANGED
|
@@ -1,28 +1,33 @@
|
|
|
1
1
|
# @scopebond/hook
|
|
2
2
|
|
|
3
|
-
The Scopebond connector for **Claude Code** and **
|
|
3
|
+
The Scopebond connector for **Claude Code**, **Cursor**, and **OpenAI Codex**. It checks every tool
|
|
4
4
|
call a coding agent makes against your policy *before it runs*, blocks the ones
|
|
5
5
|
outside policy, and records a signed receipt — locally, on the machine, and
|
|
6
6
|
optionally mirrored to your Scopebond Cloud workspace for monitoring.
|
|
7
7
|
|
|
8
|
-
- **
|
|
9
|
-
|
|
10
|
-
- **
|
|
8
|
+
- **What it does:** checks supported agent actions before they run and blocks an
|
|
9
|
+
action when it breaks your policy.
|
|
10
|
+
- **Where it works:** actions the coding agent routes through its tool system
|
|
11
11
|
(shell, file, MCP, web). A process started outside the harness is not covered.
|
|
12
12
|
|
|
13
13
|
## Install once for your machine
|
|
14
14
|
|
|
15
15
|
```
|
|
16
16
|
npm i -g @scopebond/hook
|
|
17
|
-
scopebond install # detects Cursor
|
|
17
|
+
scopebond install # detects Cursor and Codex
|
|
18
|
+
scopebond install --codex # set up Codex only
|
|
18
19
|
```
|
|
19
20
|
|
|
20
21
|
`install` sets Scopebond up **once per developer machine**, not per repository: it
|
|
21
22
|
scaffolds a user-level home (`~/.scopebond`, override `SCOPEBOND_HOME`) with a
|
|
22
23
|
signing key, a countersigning key and a starter policy, and registers the hook by
|
|
23
|
-
absolute path in your user-level `~/.claude/settings.json
|
|
24
|
-
|
|
25
|
-
|
|
24
|
+
absolute path in your user-level `~/.claude/settings.json`, `~/.cursor/hooks.json`,
|
|
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 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.
|
|
26
31
|
|
|
27
32
|
```
|
|
28
33
|
scopebond status # home, which agents are configured, cloud, receipts
|
|
@@ -43,14 +48,17 @@ Scopebond is also a Claude Code plugin (this repo is a plugin marketplace):
|
|
|
43
48
|
### Per-repository enroll (alternative)
|
|
44
49
|
|
|
45
50
|
```
|
|
46
|
-
npx @scopebond/hook init # Claude Code
|
|
51
|
+
npx @scopebond/hook init # Claude Code
|
|
52
|
+
npx @scopebond/hook init --cursor # Cursor
|
|
53
|
+
npx @scopebond/hook init --codex # Codex, then approve it once with /hooks
|
|
47
54
|
```
|
|
48
55
|
|
|
49
56
|
`init` scaffolds `.scopebond/` in the current project (a machine signing key, a
|
|
50
57
|
countersigning key, a starter policy — "protect main and production paths" — and a
|
|
51
|
-
`.gitignore` so none of it is committed) and configures `.claude/settings.json
|
|
52
|
-
`.cursor/hooks.json`. Then run one safe command in the agent and see the receipt in
|
|
53
|
-
`.scopebond/receipts.db`.
|
|
58
|
+
`.gitignore` so none of it is committed) and configures `.claude/settings.json`,
|
|
59
|
+
`.cursor/hooks.json`, or `.codex/hooks.json`. Then run one safe command in the agent and see the receipt in
|
|
60
|
+
`.scopebond/receipts.db`. `init`, `trust` and `uninstall` are meant for a person at a
|
|
61
|
+
terminal: in a script or CI, pass `--yes`.
|
|
54
62
|
|
|
55
63
|
## Connect it to your workspace (optional)
|
|
56
64
|
|
|
@@ -62,11 +70,13 @@ as `scopebond-enrollment.json`, then:
|
|
|
62
70
|
npx @scopebond/hook connect https://<your-workspace> scopebond-enrollment.json
|
|
63
71
|
```
|
|
64
72
|
|
|
73
|
+
In Windows PowerShell, use `npx.cmd` instead of `npx` if script execution policy blocks `npx.ps1`; no execution-policy change is needed. Enrollment registers both the gateway attester and the hook's separate agent signing key with proof of possession, so Cloud can verify authenticated receipts.
|
|
74
|
+
|
|
65
75
|
`connect` does the whole setup in one command: it scaffolds `.scopebond/` if needed,
|
|
66
76
|
enrolls this machine's countersigning key, stores a scoped machine credential in
|
|
67
|
-
`.scopebond/cloud.json` (a secret — never commit it), **and configures
|
|
68
|
-
for you** (it merges the hook into
|
|
69
|
-
already there — pass `--no-install` to skip, or `--
|
|
77
|
+
`.scopebond/cloud.json` (a secret — never commit it), **and configures your coding
|
|
78
|
+
agent for you** (it merges the hook into the agent's settings, preserving anything
|
|
79
|
+
already there — pass `--no-install` to skip, `--cursor` for Cursor, or `--codex` for Codex). The
|
|
70
80
|
enrollment argument can be a file, an inline blob, or JSON on stdin, so the portal
|
|
71
81
|
can hand you a single copy-paste command with nothing to save.
|
|
72
82
|
|
|
@@ -81,27 +91,69 @@ anything still queued — run it on a session-end hook (and set
|
|
|
81
91
|
Each tool call is mapped to a normalized [Action Taxonomy](https://github.com/avouro-com/scopebond)
|
|
82
92
|
action (`shell.exec`, `git.push`, `file.write`, `mcp.tool.call`, …), signed by the
|
|
83
93
|
machine key and decided against your policy by an in-process check-only gateway.
|
|
84
|
-
An allowed action is
|
|
85
|
-
|
|
94
|
+
An allowed action is recorded and left to the coding agent's normal permission
|
|
95
|
+
prompt; the hook never auto-approves it. A denied action is blocked. Unknown tools are
|
|
86
96
|
recorded as *not evaluated* and grant nothing. Anything unexpected fails closed
|
|
87
97
|
(deny) with a repair message.
|
|
88
98
|
|
|
89
99
|
Commands are stored as a scrubbed head plus a digest; file contents are never
|
|
90
100
|
stored; common secret shapes are removed before signing.
|
|
91
101
|
|
|
92
|
-
**
|
|
93
|
-
|
|
94
|
-
|
|
102
|
+
**What a shell command is checked for.** A command line is split into every command it
|
|
103
|
+
runs (`&&`, `;`, pipes, `$( )` and backticks — also inside double quotes — `bash -c`,
|
|
104
|
+
`eval`, `trap`, `su -c`, `script -c`, `watch`, `parallel`, `cmd /c`, `pwsh -Command`
|
|
105
|
+
and `-EncodedCommand`, `find -exec`, `env -S`), with shell keywords and grouping
|
|
106
|
+
(`if … then`, `for … do`, `{ … }`, `!`, `case`) and wrappers (`sudo`, `env`, `time`,
|
|
107
|
+
`nice`, `xargs`, `timeout`, `strace`, `busybox` …, each with its own option arity)
|
|
108
|
+
stripped so the real program is seen; the wrappers are checked too. Each command is
|
|
109
|
+
checked as a program, and the files it reads or writes are checked like the Read and
|
|
110
|
+
Write tools: operands of programs that output or copy file content (`cat`, `grep`,
|
|
111
|
+
`tar`, …), copies and moves (`cp`, `mv`, `rsync`, `scp`, `Copy-Item`), writers and
|
|
112
|
+
editors (`tee`, `touch`, `sed -i`, `yq -i`, `ed`, `vim`, `Set-Content`), git's own writes
|
|
113
|
+
(`checkout -- p`, `restore`, `mv`, `rm`, `config core.hooksPath`), secrets sent or staged
|
|
114
|
+
(`curl -T`, `-d @file`, `gh gist create`, `git add`) and redirections, including
|
|
115
|
+
`x>file` without spaces. Existence and metadata checks (`ls`, `test -f`, `[ -f ]`,
|
|
116
|
+
`stat`) and a secret file's name in a string are not reads. Paths are compared
|
|
117
|
+
case-insensitively; Windows `\` paths, 8.3 short names (`CLAUDE~1`), `::$DATA` streams
|
|
118
|
+
and trailing dots are normalized; and a glob, variable or brace list that could name a
|
|
119
|
+
protected file or directory (`.scope*/agent.key`, `.*/*`, `$HOME/.ssh/id_rsa`) is
|
|
120
|
+
treated as that file. Every destination of a `git push` is checked in the common
|
|
121
|
+
spellings (`refs/heads/main`, `HEAD:main`, a second refspec, `--repo`, `--all`,
|
|
122
|
+
`--mirror`); a push whose destination is not on the command line (a git alias, a
|
|
123
|
+
configured push refspec, `send-pack`) is denied, and a tags-only push is allowed. The
|
|
124
|
+
agent running the hook's own `init`, `install`, `trust`, `uninstall` or `connect` is
|
|
125
|
+
denied, and those commands also refuse a non-interactive terminal unless `--yes` is
|
|
126
|
+
passed.
|
|
127
|
+
|
|
128
|
+
**Limits.** The hook sees the command text, not what a program does at run time. It
|
|
129
|
+
does not follow a variable whose value it cannot see (`cat $FILE`), a path assembled
|
|
130
|
+
inside a script or interpreter (`python script.py`), aliases and functions defined in
|
|
131
|
+
an earlier call, git aliases from a config file, recursive reads of a parent of a
|
|
132
|
+
protected directory (`grep -r . `, `cp -r ~ /tmp`), or deletion expressed as arguments
|
|
133
|
+
(`find -delete`, `git clean`). Commands whose written files are named only inside their
|
|
134
|
+
input — `patch`, `git apply`, `git am`, `tar x`, `unzip`, `7z x`, `cpio -i` — are
|
|
135
|
+
recorded with an unevaluated write: allowed in normal mode, denied in strict mode.
|
|
136
|
+
Reading any `*.pem` file is denied, including a public certificate, because a PEM file
|
|
137
|
+
is often a private key. For stronger guarantees, run the agent behind the Scopebond
|
|
138
|
+
gateway or in a sandbox; the hook is a guardrail for a cooperating agent, not a jail.
|
|
139
|
+
|
|
140
|
+
**Strict mode.** A shell command that cannot be parsed (an unbalanced quote) or whose
|
|
141
|
+
program is only known at run time (`$cmd`, `$(…) args`) is checked with an empty
|
|
142
|
+
program name, which the starter policy denies in every mode — edit the `safe-shell`
|
|
143
|
+
clause to change that. By default a tool with no taxonomy mapping, or a write whose
|
|
144
|
+
target the command text does not name, is recorded *not evaluated* (not blocked). Add
|
|
145
|
+
`--strict` (or `SCOPEBOND_HOOK_STRICT=1`) to deny those too — fail-closed coverage for
|
|
146
|
+
anything the taxonomy does not map.
|
|
95
147
|
|
|
96
148
|
## Library
|
|
97
149
|
|
|
98
150
|
The mapper and runtime are exported for testing and embedding:
|
|
99
151
|
|
|
100
152
|
```js
|
|
101
|
-
import { mapClaudeToolUse, createHookRuntime } from "@scopebond/hook";
|
|
153
|
+
import { mapClaudeToolUse, mapCodexToolUse, createHookRuntime } from "@scopebond/hook";
|
|
102
154
|
```
|
|
103
155
|
|
|
104
|
-
`mapClaudeToolUse`
|
|
156
|
+
`mapClaudeToolUse`, `mapCursorEvent`, and `mapCodexToolUse` are pure functions (native payload →
|
|
105
157
|
normalized action). `createHookRuntime` builds the check-only gateway from a
|
|
106
158
|
policy, a machine key and a local receipt log; pass `cloud: { connection }` (from
|
|
107
159
|
`connectCloud`) to mirror receipts to a workspace.
|
package/dist/cli.js
CHANGED
|
@@ -4,13 +4,15 @@
|
|
|
4
4
|
//
|
|
5
5
|
// scopebond-hook claude evaluate a Claude Code PreToolUse call (stdin JSON)
|
|
6
6
|
// scopebond-hook cursor evaluate a Cursor hook event (stdin JSON)
|
|
7
|
-
// scopebond-hook
|
|
8
|
-
// scopebond-hook
|
|
7
|
+
// scopebond-hook codex evaluate a Codex PreToolUse call (stdin JSON)
|
|
8
|
+
// scopebond-hook init [--cursor|--codex] [--no-install] [--yes]
|
|
9
|
+
// scopebond-hook connect <url> <bundle.json> [--cursor|--codex]
|
|
9
10
|
// enroll with a Cloud workspace and start exporting
|
|
10
11
|
// scopebond-hook log [-n N] show the most recent local receipts
|
|
11
12
|
// scopebond-hook verify verify every local receipt offline against the attester key
|
|
12
13
|
// scopebond-hook test "<shell command>" show the decision for a command without recording it
|
|
13
14
|
// scopebond-hook flush deliver any queued receipts to Cloud now
|
|
15
|
+
// scopebond-hook trust [--yes] let this project's .scopebond policy govern here (pinned)
|
|
14
16
|
//
|
|
15
17
|
// Config dir: $SCOPEBOND_HOOK_DIR, else ./.scopebond
|
|
16
18
|
// Fail-closed: any error denies the action with a repair message.
|
|
@@ -20,10 +22,10 @@ import { tmpdir } from "node:os";
|
|
|
20
22
|
import { execFileSync } from "node:child_process";
|
|
21
23
|
import { verifyReceipt } from "@scopebond/gateway";
|
|
22
24
|
import { openReceiptStore, loadOrCreateAttester } from "@scopebond/gateway/node";
|
|
23
|
-
import { mapClaudeToolUse, mapCursorEvent, fillPushBranch } from "./map.js";
|
|
25
|
+
import { mapClaudeToolUse, mapCodexToolUse, mapCursorEvent, fillPushBranch } from "./map.js";
|
|
24
26
|
import { createHookRuntime } from "./runtime.js";
|
|
25
27
|
import { scaffold, harnessSnippet, installHarness } from "./init.js";
|
|
26
|
-
import { userHome, userHarnessFile, resolveConfigDir, writeHarnessConfig, removeHarnessConfig, cursorDetected, absoluteHookCommand, isHarnessConfigured, purgeHome, } from "./install.js";
|
|
28
|
+
import { userHome, userHarnessFile, resolveConfigDir, writeHarnessConfig, removeHarnessConfig, cursorDetected, codexDetected, absoluteHookCommand, isHarnessConfigured, purgeHome, trustProjectPolicy, untrustedProjectPolicy, } from "./install.js";
|
|
27
29
|
import { connectCloud, loadConnection } from "./cloud.js";
|
|
28
30
|
import { cliCommand, hookVersion } from "./version.js";
|
|
29
31
|
import { fileURLToPath } from "node:url";
|
|
@@ -93,30 +95,48 @@ function denyClaude(reason) {
|
|
|
93
95
|
process.stderr.write(`Scopebond: ${reason}\n`);
|
|
94
96
|
process.exit(2);
|
|
95
97
|
}
|
|
96
|
-
|
|
98
|
+
/** Codex accepts the structured deny response on a successful hook exit. Keeping
|
|
99
|
+
* exit 0 lets its UI show a completed policy check instead of a failed hook while
|
|
100
|
+
* still preventing the tool call. */
|
|
101
|
+
function denyCodex(reason) {
|
|
102
|
+
process.stdout.write(JSON.stringify({
|
|
103
|
+
hookSpecificOutput: { hookEventName: "PreToolUse", permissionDecision: "deny", permissionDecisionReason: reason },
|
|
104
|
+
}) + "\n");
|
|
105
|
+
process.exit(0);
|
|
106
|
+
}
|
|
107
|
+
const harnessName = (harness) => harness === "claude" ? "Claude Code" : harness === "cursor" ? "Cursor" : "Codex";
|
|
108
|
+
const harnessFileName = (harness) => harness === "claude" ? ".claude/settings.json" : harness === "cursor" ? ".cursor/hooks.json" : ".codex/hooks.json";
|
|
109
|
+
const selectedHarness = (args) => args.includes("--codex") ? "codex" : args.includes("--cursor") ? "cursor" : "claude";
|
|
110
|
+
const codexTrustStep = "Open Codex, run `/hooks`, review Scopebond, and choose Trust. Then start a new task.";
|
|
111
|
+
async function runPreToolUse(mapper, deny = denyClaude) {
|
|
97
112
|
let input;
|
|
98
113
|
try {
|
|
99
114
|
input = JSON.parse(readStdin());
|
|
100
115
|
}
|
|
101
116
|
catch {
|
|
102
|
-
|
|
117
|
+
deny("hook received invalid JSON on stdin");
|
|
103
118
|
}
|
|
104
119
|
try {
|
|
105
120
|
const cwd = input?.cwd ? String(input.cwd) : process.cwd();
|
|
106
121
|
const runtime = createHookRuntime(runtimePaths(resolveConfigDir(cwd)));
|
|
107
|
-
const decision = await runtime.evaluate(fillPushBranch(
|
|
122
|
+
const decision = await runtime.evaluate(fillPushBranch(mapper(input), currentBranch(cwd)));
|
|
108
123
|
await runtime.flush();
|
|
109
124
|
if (decision.decision === "deny")
|
|
110
|
-
|
|
111
|
-
//
|
|
112
|
-
//
|
|
113
|
-
// would suppress the user's normal review. Scopebond blocks; it does not approve.
|
|
125
|
+
deny(decision.reason);
|
|
126
|
+
// Stay silent on allow/not_evaluated so the coding agent's normal permission
|
|
127
|
+
// flow remains in charge. Scopebond blocks; it never silently approves.
|
|
114
128
|
process.exit(0);
|
|
115
129
|
}
|
|
116
130
|
catch (error) {
|
|
117
|
-
|
|
131
|
+
deny(`Scopebond hook failed closed: ${error.message}. Repair: run \`${cliCommand("init")}\`.`);
|
|
118
132
|
}
|
|
119
133
|
}
|
|
134
|
+
async function runClaude() {
|
|
135
|
+
await runPreToolUse(mapClaudeToolUse);
|
|
136
|
+
}
|
|
137
|
+
async function runCodex() {
|
|
138
|
+
await runPreToolUse(mapCodexToolUse, denyCodex);
|
|
139
|
+
}
|
|
120
140
|
async function runCursor() {
|
|
121
141
|
let input = {};
|
|
122
142
|
try {
|
|
@@ -143,22 +163,49 @@ async function runCursor() {
|
|
|
143
163
|
process.stdout.write(JSON.stringify({ permission, agentMessage: message }) + "\n");
|
|
144
164
|
process.exit(0);
|
|
145
165
|
}
|
|
166
|
+
/** `init`, `trust` and `uninstall` change what governs the agent, so they are for a
|
|
167
|
+
* person at a terminal. A coding agent's shell is not interactive: without a TTY on
|
|
168
|
+
* stdin they refuse unless `--yes` is passed (for scripts and CI). The starter policy
|
|
169
|
+
* also denies the agent running them. */
|
|
170
|
+
function requireInteractive(command, args) {
|
|
171
|
+
if (process.stdin.isTTY || args.includes("--yes"))
|
|
172
|
+
return;
|
|
173
|
+
console.error(`scopebond ${command} changes what governs your coding agent, so it must be run from an interactive terminal.`);
|
|
174
|
+
console.error(`In a script or CI, pass --yes: ${cliCommand(`${command} --yes`)}`);
|
|
175
|
+
process.exit(1);
|
|
176
|
+
}
|
|
146
177
|
function runInit(args) {
|
|
147
|
-
|
|
178
|
+
requireInteractive("init", args);
|
|
179
|
+
const harness = selectedHarness(args);
|
|
148
180
|
const dir = configDir();
|
|
149
181
|
const { agentKid, policyPath } = scaffold(dir, { force: args.includes("--force") });
|
|
150
182
|
console.log(`Scopebond hook enrolled in ${dir}`);
|
|
151
183
|
console.log(` machine key ${agentKid}`);
|
|
152
184
|
console.log(` policy ${policyPath} (starter — edit the limits)`);
|
|
185
|
+
// With a user-level install present, a project policy governs only once trusted.
|
|
186
|
+
// Running init here is that decision, so pin this policy now.
|
|
187
|
+
if (!process.env.SCOPEBOND_HOOK_DIR && existsSync(join(userHome(), "policy.json"))) {
|
|
188
|
+
trustProjectPolicy(dir);
|
|
189
|
+
console.log(` trusted overrides ${userHome()} here; after editing it, run \`${cliCommand("trust")}\``);
|
|
190
|
+
}
|
|
153
191
|
console.log("");
|
|
154
192
|
// Configure the agent automatically by default (idempotent), so there is no
|
|
155
193
|
// hand-editing step; --no-install prints the snippet instead.
|
|
156
194
|
if (!args.includes("--no-install")) {
|
|
157
|
-
|
|
158
|
-
|
|
195
|
+
let file;
|
|
196
|
+
try {
|
|
197
|
+
file = installHarness(harness);
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
console.error(error.message);
|
|
201
|
+
process.exit(1);
|
|
202
|
+
}
|
|
203
|
+
console.log(`✓ ${harnessName(harness)} configured in ${file}`);
|
|
204
|
+
if (harness === "codex")
|
|
205
|
+
console.log(`\nOne last step: ${codexTrustStep}`);
|
|
159
206
|
}
|
|
160
207
|
else {
|
|
161
|
-
console.log(`Add this to your ${harness
|
|
208
|
+
console.log(`Add this to your ${harnessFileName(harness)}:`);
|
|
162
209
|
console.log(harnessSnippet(harness));
|
|
163
210
|
}
|
|
164
211
|
console.log("");
|
|
@@ -277,9 +324,9 @@ async function runConnect(args) {
|
|
|
277
324
|
const positional = args.filter((a) => !a.startsWith("--"));
|
|
278
325
|
const url = positional[0];
|
|
279
326
|
const bundleArg = positional[1];
|
|
280
|
-
const harness = args
|
|
327
|
+
const harness = selectedHarness(args);
|
|
281
328
|
if (!url) {
|
|
282
|
-
console.error("usage: scopebond-hook connect <workspace-url> <enrollment> [--cursor] [--no-install]");
|
|
329
|
+
console.error("usage: scopebond-hook connect <workspace-url> <enrollment> [--claude|--cursor|--codex] [--no-install]");
|
|
283
330
|
process.exit(1);
|
|
284
331
|
}
|
|
285
332
|
const dir = configDir();
|
|
@@ -303,10 +350,12 @@ async function runConnect(args) {
|
|
|
303
350
|
// caller opts out. This removes the "paste this snippet" step.
|
|
304
351
|
if (!args.includes("--no-install")) {
|
|
305
352
|
const file = installHarness(harness);
|
|
306
|
-
console.log(`✓ ${harness
|
|
353
|
+
console.log(`✓ ${harnessName(harness)} configured in ${file}`);
|
|
354
|
+
if (harness === "codex")
|
|
355
|
+
console.log(`\nOne last step: ${codexTrustStep}`);
|
|
307
356
|
}
|
|
308
357
|
else {
|
|
309
|
-
console.log(`Add this to your ${harness
|
|
358
|
+
console.log(`Add this to your ${harnessFileName(harness)}:`);
|
|
310
359
|
console.log(harnessSnippet(harness));
|
|
311
360
|
}
|
|
312
361
|
console.log("");
|
|
@@ -326,8 +375,10 @@ async function runFlush() {
|
|
|
326
375
|
const runtime = createHookRuntime(runtimePaths(dir));
|
|
327
376
|
await runtime.exporter?.flush();
|
|
328
377
|
const status = runtime.exporter?.status();
|
|
378
|
+
runtime.exporter?.stop();
|
|
329
379
|
console.log(`flushed; ${status?.pending ?? 0} receipt(s) still pending${status?.lastError ? ` (last error: ${status.lastError})` : ""}`);
|
|
330
|
-
|
|
380
|
+
// Let pending HTTP handles close normally (forced exit can abort on Windows).
|
|
381
|
+
process.exitCode = status && status.pending > 0 ? 1 : 0;
|
|
331
382
|
}
|
|
332
383
|
/** The absolute path to this CLI file, for registering the hook by absolute path. */
|
|
333
384
|
function cliPath() {
|
|
@@ -343,21 +394,30 @@ function runInstall(args) {
|
|
|
343
394
|
console.log(` machine key ${agentKid}`);
|
|
344
395
|
console.log(` policy ${policyPath} (starter — edit the limits)`);
|
|
345
396
|
console.log("");
|
|
346
|
-
const harnesses = args.includes("--
|
|
347
|
-
? ["cursor"]
|
|
348
|
-
|
|
397
|
+
const harnesses = args.includes("--codex") ? ["codex"]
|
|
398
|
+
: args.includes("--cursor") ? ["cursor"]
|
|
399
|
+
: args.includes("--claude") ? ["claude"]
|
|
400
|
+
: ["claude", ...(cursorDetected() ? ["cursor"] : []), ...(codexDetected() ? ["codex"] : [])];
|
|
349
401
|
if (!args.includes("--no-install")) {
|
|
350
402
|
for (const h of harnesses) {
|
|
351
|
-
|
|
352
|
-
|
|
403
|
+
try {
|
|
404
|
+
const file = writeHarnessConfig(userHarnessFile(h), h, absoluteHookCommand(cliPath(), h));
|
|
405
|
+
console.log(`✓ ${harnessName(h)} configured in ${file}`);
|
|
406
|
+
}
|
|
407
|
+
catch (error) {
|
|
408
|
+
console.error(`✗ ${harnessName(h)}: ${error.message}`);
|
|
409
|
+
process.exitCode = 1;
|
|
410
|
+
}
|
|
353
411
|
}
|
|
354
412
|
}
|
|
355
413
|
else {
|
|
356
414
|
console.log("Add this to your user-level agent config:");
|
|
357
415
|
console.log(harnessSnippet(harnesses[0]));
|
|
358
416
|
}
|
|
417
|
+
if (harnesses.includes("codex"))
|
|
418
|
+
console.log(`\nOne last step for Codex: ${codexTrustStep}`);
|
|
359
419
|
console.log("");
|
|
360
|
-
console.log(
|
|
420
|
+
console.log(`A project's own .scopebond policy applies only after you trust it there (${cliCommand("trust")}).`);
|
|
361
421
|
console.log(`Check it: ${cliCommand("doctor")} · see decisions: ${cliCommand("log")}`);
|
|
362
422
|
console.log(`To send receipts to a workspace: ${cliCommand("connect <workspace-url> <enrollment>")}`);
|
|
363
423
|
}
|
|
@@ -366,13 +426,18 @@ function runStatus() {
|
|
|
366
426
|
const installed = existsSync(join(home, "policy.json"));
|
|
367
427
|
const claude = isHarnessConfigured(userHarnessFile("claude"));
|
|
368
428
|
const cursor = isHarnessConfigured(userHarnessFile("cursor"));
|
|
429
|
+
const codex = isHarnessConfigured(userHarnessFile("codex"));
|
|
369
430
|
const connected = !!loadConnection(resolveConfigDir(process.cwd()));
|
|
370
431
|
const dbPath = join(resolveConfigDir(process.cwd()), "receipts.db");
|
|
371
432
|
console.log(`Scopebond hook ${hookVersion()}`);
|
|
372
433
|
console.log(` user home ${home} ${installed ? "(installed)" : "(not installed — run `scopebond install`)"}`);
|
|
373
434
|
console.log(` active config ${resolveConfigDir(process.cwd())}`);
|
|
374
|
-
|
|
435
|
+
const ignored = untrustedProjectPolicy(process.cwd());
|
|
436
|
+
if (ignored)
|
|
437
|
+
console.log(` project policy ${ignored} ignored — not trusted (run \`${cliCommand("trust")}\` to use it)`);
|
|
438
|
+
console.log(` Claude Code ${claude ? "configured" : "not configured"}`);
|
|
375
439
|
console.log(` Cursor ${cursor ? "configured" : cursorDetected() ? "detected, not configured" : "not detected"}`);
|
|
440
|
+
console.log(` Codex ${codex ? "configured (approve once with /hooks)" : codexDetected() ? "detected, not configured" : "not detected"}`);
|
|
376
441
|
console.log(` cloud workspace ${connected ? "connected" : "not connected (local only)"}`);
|
|
377
442
|
console.log(` local receipts ${existsSync(dbPath) ? dbPath : "none yet"}`);
|
|
378
443
|
}
|
|
@@ -391,6 +456,13 @@ async function runDoctor() {
|
|
|
391
456
|
console.log(` active config ${active} ${hasPolicy ? "ok" : "no policy (run `scopebond install` or `init`)"}`);
|
|
392
457
|
if (!hasPolicy)
|
|
393
458
|
problems.push("no policy found in the active config dir");
|
|
459
|
+
const ignored = untrustedProjectPolicy(process.cwd());
|
|
460
|
+
if (ignored)
|
|
461
|
+
console.log(` project policy ${ignored} IGNORED — not trusted (never trusted, or edited since). Review it, then \`${cliCommand("trust")}\``);
|
|
462
|
+
const codex = isHarnessConfigured(userHarnessFile("codex"));
|
|
463
|
+
console.log(` Codex hook ${codex ? "configured" : codexDetected() ? "not configured — run `scopebond install --codex`" : "not detected"}`);
|
|
464
|
+
if (codex)
|
|
465
|
+
console.log(` Codex approval run /hooks in Codex and approve Scopebond once`);
|
|
394
466
|
const connection = loadConnection(active);
|
|
395
467
|
if (!connection) {
|
|
396
468
|
console.log(` cloud not connected (local only) — receipts stay on this machine`);
|
|
@@ -407,11 +479,12 @@ async function runDoctor() {
|
|
|
407
479
|
console.log(` cloud ${connection.url} — ${reachable}`);
|
|
408
480
|
}
|
|
409
481
|
console.log(problems.length ? `\n${problems.length} problem(s): ${problems.join("; ")}` : `\nAll good.`);
|
|
410
|
-
process.
|
|
482
|
+
process.exitCode = problems.length ? 1 : 0;
|
|
411
483
|
}
|
|
412
484
|
function runUninstall(args) {
|
|
485
|
+
requireInteractive("uninstall", args);
|
|
413
486
|
let removed = 0;
|
|
414
|
-
for (const h of ["claude", "cursor"]) {
|
|
487
|
+
for (const h of ["claude", "cursor", "codex"]) {
|
|
415
488
|
if (removeHarnessConfig(userHarnessFile(h))) {
|
|
416
489
|
console.log(`✓ removed the Scopebond hook from ${userHarnessFile(h)}`);
|
|
417
490
|
removed++;
|
|
@@ -426,6 +499,20 @@ function runUninstall(args) {
|
|
|
426
499
|
else
|
|
427
500
|
console.log(`Kept ${userHome()} (keys, policy, receipts). Use --purge to remove it too.`);
|
|
428
501
|
}
|
|
502
|
+
/** `trust` — let this project's .scopebond policy govern here instead of the user
|
|
503
|
+
* home, pinned to its current contents. Run by the user, not the agent: the file it
|
|
504
|
+
* writes lives in the user home, which the starter policy write-protects. */
|
|
505
|
+
function runTrust(args) {
|
|
506
|
+
requireInteractive("trust", args);
|
|
507
|
+
const dir = join(process.cwd(), ".scopebond");
|
|
508
|
+
if (!existsSync(join(dir, "policy.json"))) {
|
|
509
|
+
console.error(`no project policy at ${join(dir, "policy.json")}`);
|
|
510
|
+
process.exit(1);
|
|
511
|
+
}
|
|
512
|
+
const digest = trustProjectPolicy(dir);
|
|
513
|
+
console.log(`✓ trusted ${join(dir, "policy.json")} (sha256 ${digest.slice(0, 12)}…)`);
|
|
514
|
+
console.log("It governs agents in this project until it changes; after any edit, review it and run trust again.");
|
|
515
|
+
}
|
|
429
516
|
function runLogin() {
|
|
430
517
|
console.log("Device-code login is not available yet.");
|
|
431
518
|
console.log(`For now, connect with a one-time enrollment from your workspace:`);
|
|
@@ -439,6 +526,9 @@ if (cmd === "claude") {
|
|
|
439
526
|
else if (cmd === "cursor") {
|
|
440
527
|
await runCursor();
|
|
441
528
|
}
|
|
529
|
+
else if (cmd === "codex") {
|
|
530
|
+
await runCodex();
|
|
531
|
+
}
|
|
442
532
|
else if (cmd === "init") {
|
|
443
533
|
runInit(rest);
|
|
444
534
|
}
|
|
@@ -472,8 +562,11 @@ else if (cmd === "uninstall") {
|
|
|
472
562
|
else if (cmd === "login") {
|
|
473
563
|
runLogin();
|
|
474
564
|
}
|
|
565
|
+
else if (cmd === "trust") {
|
|
566
|
+
runTrust(rest);
|
|
567
|
+
}
|
|
475
568
|
else {
|
|
476
|
-
console.error("usage: scopebond <claude|cursor|init|install|connect|log|verify|test|flush|status|doctor|uninstall|login> [--cursor] [--claude] [--force] [--strict] [--no-install] [--purge]");
|
|
569
|
+
console.error("usage: scopebond <claude|cursor|codex|init|install|connect|log|verify|test|flush|status|doctor|uninstall|login|trust> [--cursor] [--claude] [--codex] [--force] [--strict] [--no-install] [--purge] [--yes]");
|
|
477
570
|
process.exit(1);
|
|
478
571
|
}
|
|
479
572
|
//# sourceMappingURL=cli.js.map
|