@hasna/hooks 0.10.6 → 0.10.8
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 +82 -1
- package/bin/hooks-mcp.js +100 -44
- package/bin/index.js +7835 -6947
- package/bin/native-safety-entry.js +196 -15
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1082 -68
- package/dist/lib/claude-safety-check.d.ts +32 -0
- package/dist/lib/codex-safety-check.d.ts +58 -0
- package/dist/lib/codex-safety-trust.d.ts +54 -0
- package/dist/lib/codex-settings.d.ts +5 -1
- package/dist/lib/installer.d.ts +8 -0
- package/dist/lib/native-safety-registration.d.ts +52 -0
- package/dist/lib/native-safety.d.ts +4 -0
- package/dist/native-safety.d.ts +6 -0
- package/dist/native-safety.js +18 -0
- package/hooks/hook-workspace-repos-guard/README.md +12 -0
- package/hooks/hook-workspace-repos-guard/src/hook.ts +247 -14
- package/package.json +2 -2
- package/scripts/ensure-profiles-dir.mjs +65 -1
package/README.md
CHANGED
|
@@ -92,6 +92,49 @@ Repair unsafe installation permissions with a verified package installer, then
|
|
|
92
92
|
check again. This check alone does not prove hosted authentication, native hook
|
|
93
93
|
trust or a running agent's interception.
|
|
94
94
|
|
|
95
|
+
## Planned safety registration
|
|
96
|
+
|
|
97
|
+
For repeatable global setup, use the owning installer through its plan/apply
|
|
98
|
+
commands. Planning leaves native settings unchanged and returns a compact digest.
|
|
99
|
+
|
|
100
|
+
```bash
|
|
101
|
+
hooks safety registration-plan --target codex
|
|
102
|
+
hooks safety registration-apply --target codex --expected-plan-digest PLAN_SHA256
|
|
103
|
+
# The same two commands support --target claude.
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
Apply checks the exact predecessor, retains an owner-only original and intent,
|
|
107
|
+
and reads back the result. An identical registration returns unchanged without
|
|
108
|
+
rewriting its settings. The planner honors `CODEX_HOME` and `CLAUDE_CONFIG_DIR`;
|
|
109
|
+
conflicting Hooks path overrides refuse. Missing native settings directories are
|
|
110
|
+
created only on apply. Disabled or asynchronous guards, unsafe paths, duplicate
|
|
111
|
+
JSON keys and numbers that cannot be preserved exactly refuse before a write.
|
|
112
|
+
Managed default Claude settings continue through Skills' coordinated writer.
|
|
113
|
+
|
|
114
|
+
The SDK exports `planNativeSafetyRegistration` and
|
|
115
|
+
`applyNativeSafetyRegistration`. Both return `nativeAdoptionVerified: false`.
|
|
116
|
+
A changed or uncertain apply requires a fresh read-only plan and inspection of
|
|
117
|
+
its recorded operation; no automatic retry or direct overwrite is performed.
|
|
118
|
+
|
|
119
|
+
After registration, Codex trust uses its supported native configuration writer:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
hooks safety trust-plan --target codex --cwd /path/to/project --codex-command /absolute/path/to/native/codex
|
|
123
|
+
hooks safety trust-apply --target codex --cwd /path/to/project --codex-command /absolute/path/to/native/codex --expected-plan-digest PLAN_SHA256
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
The exact definition is verified before trusting and enabling only that guard.
|
|
127
|
+
Unrelated configuration is preserved, and an already trusted, enabled guard
|
|
128
|
+
does not cause a write. A disabled native hooks feature is a refusal, not an
|
|
129
|
+
implicit feature change. Neither registration nor trust proves that an existing
|
|
130
|
+
agent has adopted the change.
|
|
131
|
+
|
|
132
|
+
For Claude, `hooks safety verify --target claude --cwd /path/to/project` checks
|
|
133
|
+
the selected settings and bundled supervisor. Its result explicitly leaves
|
|
134
|
+
runtime-policy verification, native discovery and native adoption false;
|
|
135
|
+
MDM/server policy, workspace trust and launch flags require native acceptance.
|
|
136
|
+
|
|
137
|
+
|
|
95
138
|
## Codex safety readiness
|
|
96
139
|
|
|
97
140
|
```bash
|
|
@@ -350,8 +393,46 @@ commands are credential-free and return compact JSON. Verification explicitly
|
|
|
350
393
|
reports `nativeAdoptionVerified: false`: a fresh native Sumi tool invocation is
|
|
351
394
|
still required to prove that the installed harness uses this binding.
|
|
352
395
|
|
|
396
|
+
The binding is provisioned from the package's own install, upgrade and safety
|
|
397
|
+
surfaces, so a rollout cannot leave Sumi's fail-closed check unbound:
|
|
398
|
+
|
|
399
|
+
- the package lifecycle script (`postinstall`) provisions it for a packaged
|
|
400
|
+
install — a copy inside a `node_modules` directory, never a source checkout;
|
|
401
|
+
- `hooks install` and `hooks upgrade` apply the same step after a successful
|
|
402
|
+
hook install or package upgrade, and report it as `sumiBinding` in `--json`;
|
|
403
|
+
- `hooks safety provision` runs it on demand and is credential-free.
|
|
404
|
+
|
|
405
|
+
Every surface is idempotent and shares one policy: a missing binding is created,
|
|
406
|
+
a byte-identical one is written nowhere, a different binding that still verifies
|
|
407
|
+
is left intact, and one whose pinned runtime or worker no longer verifies (an
|
|
408
|
+
upgrade rewrote the worker, or the runtime prefix was pruned) is re-registered
|
|
409
|
+
with the previous record preserved beside it as `<path>.before-<sha256>`. An
|
|
410
|
+
unreadable or unrecognized record is refused rather than overwritten, and
|
|
411
|
+
provisioning never fails the calling command — it reports a stable `code`,
|
|
412
|
+
because an unavailable Trash guard must not break a hook install. bun blocks
|
|
413
|
+
lifecycle scripts for untrusted dependencies and `--ignore-scripts` skips them,
|
|
414
|
+
so the CLI surfaces carry the same policy wherever the lifecycle script does not
|
|
415
|
+
run. A harness rollout that installs this package with scripts disabled must run
|
|
416
|
+
one of those surfaces; that is the harness owner's half of this contract.
|
|
417
|
+
|
|
418
|
+
The binding pins the absolute paths and sha256 of the Bun runtime and the worker
|
|
419
|
+
bundle as they exist when it is registered, and every evaluation re-verifies
|
|
420
|
+
those exact bytes. A worker inside a tree a package upgrade rewrites in place —
|
|
421
|
+
bun's global install root, its cache, or an isolated `node_modules/.bun/…` store —
|
|
422
|
+
is therefore only valid until that tree is upgraded; the next rewrite refuses
|
|
423
|
+
every command until provisioning runs again. Provisioning still closes the
|
|
424
|
+
immediate gap (an unbound station has no shell at all) but reports it: every
|
|
425
|
+
result carries `durable`, and a false value prints the repair step. Provision
|
|
426
|
+
rollouts from a stable, versioned install tree (for example the fleet's
|
|
427
|
+
`~/.hasna/hooks/runtime/<version>-copyfile` layout) and re-run
|
|
428
|
+
`hooks safety provision` after upgrading a tree the binding points at.
|
|
429
|
+
|
|
353
430
|
The owner-only `~/.hasna/hooks/native/sumi-trash-guard.json` pins the supervisor's
|
|
354
|
-
runtime, worker, home and PATH.
|
|
431
|
+
runtime, worker, home and PATH. Owner and mode are part of the trust decision: a
|
|
432
|
+
record that is not owned by the current user, or that group or others can read or
|
|
433
|
+
write, is refused with the failing property named — never trusted, adopted or
|
|
434
|
+
silently replaced — because a rewritable binding can point the supervisor at a
|
|
435
|
+
different worker. Identical setup is a no-op. An upgrade requires
|
|
355
436
|
`--expected-binding-sha256 <current-digest>` and preserves the previous record.
|
|
356
437
|
An interrupted lock or unexpected current record requires reconciliation; setup
|
|
357
438
|
does not overwrite it. This file is native configuration, not a Hooks app store.
|
package/bin/hooks-mcp.js
CHANGED
|
@@ -3104,7 +3104,7 @@ function checkScriptHash(name, actual) {
|
|
|
3104
3104
|
// src/lib/codex-settings.ts
|
|
3105
3105
|
import { closeSync as closeSync2, existsSync as existsSync8, lstatSync as lstatSync2, mkdirSync as mkdirSync4, openSync as openSync2, readFileSync as readFileSync5, renameSync as renameSync2, unlinkSync, writeFileSync as writeFileSync3 } from "fs";
|
|
3106
3106
|
import { dirname as dirname4 } from "path";
|
|
3107
|
-
import { randomUUID } from "crypto";
|
|
3107
|
+
import { createHash as createHash3, randomUUID } from "crypto";
|
|
3108
3108
|
var snapshots = new WeakMap;
|
|
3109
3109
|
function current(path) {
|
|
3110
3110
|
try {
|
|
@@ -3139,11 +3139,24 @@ function readCodexSettings(path) {
|
|
|
3139
3139
|
snapshots.set(settings, { path, raw });
|
|
3140
3140
|
return settings;
|
|
3141
3141
|
}
|
|
3142
|
-
function
|
|
3142
|
+
function assertSettingsPreimage(raw, expected) {
|
|
3143
|
+
if (expected === undefined)
|
|
3144
|
+
return;
|
|
3145
|
+
if (!/^(?:absent|[a-f0-9]{64})$/.test(expected) || (raw === null ? "absent" : createHash3("sha256").update(raw).digest("hex")) !== expected) {
|
|
3146
|
+
throw new Error("Hook settings changed since planning; no settings were written.");
|
|
3147
|
+
}
|
|
3148
|
+
}
|
|
3149
|
+
function writeCodexSettings(path, settings, expectedSha256) {
|
|
3143
3150
|
const snapshot = snapshots.get(settings);
|
|
3144
3151
|
if (!snapshot || snapshot.path !== path)
|
|
3145
3152
|
throw new Error("Codex settings require a complete current read before writing.");
|
|
3146
|
-
|
|
3153
|
+
assertSettingsPreimage(snapshot.raw, expectedSha256);
|
|
3154
|
+
writeHookSettingsPreimage(path, settings, snapshot.raw);
|
|
3155
|
+
snapshots.set(settings, { path, raw: JSON.stringify(settings, null, 2) + `
|
|
3156
|
+
` });
|
|
3157
|
+
}
|
|
3158
|
+
function writeHookSettingsPreimage(path, settings, before) {
|
|
3159
|
+
mkdirSync4(dirname4(path), { recursive: true, mode: 448 });
|
|
3147
3160
|
const lock = `${path}.hooks-write.lock`;
|
|
3148
3161
|
let fd;
|
|
3149
3162
|
try {
|
|
@@ -3153,15 +3166,14 @@ function writeCodexSettings(path, settings) {
|
|
|
3153
3166
|
}
|
|
3154
3167
|
const temp = `${path}.${randomUUID()}.tmp`;
|
|
3155
3168
|
try {
|
|
3156
|
-
if (current(path) !==
|
|
3157
|
-
throw new Error("
|
|
3169
|
+
if (current(path) !== before)
|
|
3170
|
+
throw new Error("Hook settings changed since reading; re-read before retrying.");
|
|
3158
3171
|
const next = JSON.stringify(settings, null, 2) + `
|
|
3159
3172
|
`;
|
|
3160
3173
|
writeFileSync3(temp, next, { flag: "wx", mode: 384 });
|
|
3161
|
-
if (current(path) !==
|
|
3162
|
-
throw new Error("
|
|
3174
|
+
if (current(path) !== before)
|
|
3175
|
+
throw new Error("Hook settings changed since reading; re-read before retrying.");
|
|
3163
3176
|
renameSync2(temp, path);
|
|
3164
|
-
snapshots.set(settings, { path, raw: next });
|
|
3165
3177
|
} finally {
|
|
3166
3178
|
if (existsSync8(temp))
|
|
3167
3179
|
unlinkSync(temp);
|
|
@@ -3173,19 +3185,20 @@ function writeCodexSettings(path, settings) {
|
|
|
3173
3185
|
// src/lib/claude-skills-coordination.ts
|
|
3174
3186
|
import { createHash as createHash5 } from "crypto";
|
|
3175
3187
|
|
|
3176
|
-
// ../../node_modules/.bun/@hasna+skills@0.9.
|
|
3188
|
+
// ../../node_modules/.bun/@hasna+skills@0.9.17+a00d3064559565bb/node_modules/@hasna/skills/dist/index.js
|
|
3177
3189
|
import { existsSync as existsSync9 } from "fs";
|
|
3178
3190
|
import { homedir as homedir4 } from "os";
|
|
3179
3191
|
import { join as join8, resolve as resolve3 } from "path";
|
|
3180
3192
|
import { homedir as pathsResolverHomedir2 } from "os";
|
|
3181
3193
|
import { join as pathsResolverJoin2 } from "path";
|
|
3182
|
-
import { createHash as
|
|
3194
|
+
import { createHash as createHash4 } from "crypto";
|
|
3183
3195
|
import { existsSync as existsSync52, readdirSync as readdirSync3, readFileSync as readFileSync42, statSync as statSync3 } from "fs";
|
|
3184
3196
|
import { join as join52, sep as sep2 } from "path";
|
|
3185
3197
|
import { basename as basename2, isAbsolute as isAbsolute22, resolve as resolve22 } from "path";
|
|
3186
3198
|
import { createHash as createHash16 } from "crypto";
|
|
3187
3199
|
import { lstatSync as lstatSync20, readFileSync as readFileSync16, readdirSync as readdirSync9, realpathSync as realpathSync4 } from "fs";
|
|
3188
3200
|
import { basename as basename8, dirname as dirname16, isAbsolute as isAbsolute14, join as join26, resolve as resolve14, sep as sep5 } from "path";
|
|
3201
|
+
import { isDeepStrictEqual as isDeepStrictEqual3 } from "util";
|
|
3189
3202
|
import { dirname as dirname32, isAbsolute as isAbsolute3, join as join72, resolve as resolve32 } from "path";
|
|
3190
3203
|
import { lstatSync as lstatSync3, readFileSync as readFileSync6, opendirSync } from "fs";
|
|
3191
3204
|
import { createHash as createHash22 } from "crypto";
|
|
@@ -3194,7 +3207,7 @@ import { dirname as dirname42, isAbsolute as isAbsolute4, join as join82, resolv
|
|
|
3194
3207
|
import { createHash as createHash32 } from "crypto";
|
|
3195
3208
|
import { closeSync as closeSync3, constants as constants2, fstatSync as fstatSync2, lstatSync as lstatSync5, openSync as openSync3, readSync } from "fs";
|
|
3196
3209
|
import { dirname as dirname5, isAbsolute as isAbsolute5, resolve as resolve5 } from "path";
|
|
3197
|
-
import { createHash as
|
|
3210
|
+
import { createHash as createHash42 } from "crypto";
|
|
3198
3211
|
import { lstatSync as lstatSync6, readlinkSync } from "fs";
|
|
3199
3212
|
import { dirname as dirname6, isAbsolute as isAbsolute6, join as join9, parse, resolve as resolve6, sep as sep22 } from "path";
|
|
3200
3213
|
import { closeSync as closeSync12, constants as constants9, fstatSync as fstatSync12, lstatSync as lstatSync17, openSync as openSync12, opendirSync as opendirSync5, readSync as readSync8 } from "fs";
|
|
@@ -10322,7 +10335,7 @@ function* bundleHashParts(files) {
|
|
|
10322
10335
|
yield new TextEncoder().encode("\x00");
|
|
10323
10336
|
}
|
|
10324
10337
|
function hashBundleFiles(files) {
|
|
10325
|
-
const hash =
|
|
10338
|
+
const hash = createHash4(CONTENT_HASH_ALGORITHM);
|
|
10326
10339
|
for (const part of bundleHashParts(files))
|
|
10327
10340
|
hash.update(part);
|
|
10328
10341
|
return hash.digest("hex");
|
|
@@ -10666,8 +10679,8 @@ function assertAgentPolicyCollections(policy) {
|
|
|
10666
10679
|
text(source.path);
|
|
10667
10680
|
requireBound(source.sha256 === null || typeof source.sha256 === "string" && /^[a-f0-9]{64}$/.test(source.sha256));
|
|
10668
10681
|
if (source.hashMode !== undefined)
|
|
10669
|
-
requireBound(["bytes", "path-bytes", "claude-plugin-registry", "claude-marketplace-registry", "claude-settings-v1", "claude-settings-v2", "claude-marketplace-registry-v2"].includes(source.hashMode) && source.format === undefined && source.fields === undefined && (source.hashMode === "bytes" || source.sha256 !== null));
|
|
10670
|
-
if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2")
|
|
10682
|
+
requireBound(["bytes", "path-bytes", "claude-plugin-registry", "claude-marketplace-registry", "claude-settings-v1", "claude-settings-v2", "claude-settings-v3", "claude-marketplace-registry-v2"].includes(source.hashMode) && source.format === undefined && source.fields === undefined && (source.hashMode === "bytes" || source.sha256 !== null));
|
|
10683
|
+
if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3")
|
|
10671
10684
|
requireBound(agent === "claude" && value.agent === "claude" && value.method === "reviewed" && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute22(source.path) && resolve22(source.path) === source.path && basename2(source.path) === "settings.json");
|
|
10672
10685
|
if (source.hashMode === "claude-marketplace-registry" || source.hashMode === "claude-marketplace-registry-v2")
|
|
10673
10686
|
requireBound(agent === "claude" && value.agent === "claude" && value.method === "reviewed" && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute22(source.path) && resolve22(source.path) === source.path && basename2(source.path) === "known_marketplaces.json");
|
|
@@ -11158,11 +11171,11 @@ function hashDiscoveryPathFile(path, budget, changes) {
|
|
|
11158
11171
|
const after = trace(path);
|
|
11159
11172
|
if (JSON.stringify(before.rows) !== JSON.stringify(after.rows) || before.file !== after.file)
|
|
11160
11173
|
throw new Error("Discovery path input changed during read");
|
|
11161
|
-
return
|
|
11174
|
+
return createHash42("sha256").update(JSON.stringify({ version: 1, path, rows: before.rows, content })).digest("hex");
|
|
11162
11175
|
}
|
|
11163
11176
|
var package_default = {
|
|
11164
11177
|
name: "@hasna/skills",
|
|
11165
|
-
version: "0.9.
|
|
11178
|
+
version: "0.9.17",
|
|
11166
11179
|
description: "Skills library for AI coding agents",
|
|
11167
11180
|
type: "module",
|
|
11168
11181
|
bin: {
|
|
@@ -12017,7 +12030,7 @@ function assertClaudeHookEventsReplacement(before, replacement, events) {
|
|
|
12017
12030
|
need2(JSON.stringify(withoutOwnedEvents(original)) === JSON.stringify(withoutOwnedEvents(next)), `${label} update changed settings outside hooks.${label}`);
|
|
12018
12031
|
}
|
|
12019
12032
|
var PERSISTED_EFFORT_LEVELS = new Set(["low", "medium", "high", "xhigh"]);
|
|
12020
|
-
function inferencePreference(key, child) {
|
|
12033
|
+
function inferencePreference(key, child, version) {
|
|
12021
12034
|
if (key === "skipDangerousModePermissionPrompt") {
|
|
12022
12035
|
need2(child.kind === "literal" && (child.value === "true" || child.value === "false"), "contains an invalid dialog acknowledgment");
|
|
12023
12036
|
return false;
|
|
@@ -12030,7 +12043,7 @@ function inferencePreference(key, child) {
|
|
|
12030
12043
|
return false;
|
|
12031
12044
|
need2(child.kind === "object", "requires an object of model preferences");
|
|
12032
12045
|
child.entries = child.entries.filter(([model, preferences]) => {
|
|
12033
|
-
if (!BUILTIN_MODEL_SELECTIONS.has(model) || preferences.kind !== "object" || preferences.entries.length !== 1 || preferences.entries[0][0] !== "effortLevel")
|
|
12046
|
+
if (version === 2 && !BUILTIN_MODEL_SELECTIONS.has(model) || preferences.kind !== "object" || preferences.entries.length !== 1 || preferences.entries[0][0] !== "effortLevel")
|
|
12034
12047
|
return true;
|
|
12035
12048
|
const effort = preferences.entries[0][1];
|
|
12036
12049
|
need2(effort.kind === "string" && PERSISTED_EFFORT_LEVELS.has(effort.value), "contains an invalid persisted model effort level");
|
|
@@ -12041,7 +12054,7 @@ function inferencePreference(key, child) {
|
|
|
12041
12054
|
function settingsDigest(text2, version = 1) {
|
|
12042
12055
|
const value = parse3(text2);
|
|
12043
12056
|
value.entries = value.entries.filter(([key, child]) => {
|
|
12044
|
-
if (version
|
|
12057
|
+
if (version !== 1 && inferencePreference(key, child, version))
|
|
12045
12058
|
return false;
|
|
12046
12059
|
if (key === "model") {
|
|
12047
12060
|
need2(child.kind === "string", "contains an invalid model selection");
|
|
@@ -12079,6 +12092,16 @@ function hashClaudeSettingsReplacementV2(text2, budget) {
|
|
|
12079
12092
|
budget.remaining -= bytes;
|
|
12080
12093
|
return settingsDigest(text2, 2);
|
|
12081
12094
|
}
|
|
12095
|
+
function captureClaudeSettingsV3(path, budget = { remaining: 256 * 1024 * 1024 }) {
|
|
12096
|
+
return { path, hashMode: "claude-settings-v3", sha256: settingsDigest(readSettings(path, budget), 3) };
|
|
12097
|
+
}
|
|
12098
|
+
function hashClaudeSettingsReplacementV3(text2, budget) {
|
|
12099
|
+
need2(typeof text2 === "string", "requires settings text");
|
|
12100
|
+
const bytes = Buffer.byteLength(text2);
|
|
12101
|
+
need2(bytes <= CLAUDE_SETTINGS_WITNESS_LIMITS.bytes && Number.isSafeInteger(budget.remaining) && budget.remaining >= bytes, "exceeds its byte limit");
|
|
12102
|
+
budget.remaining -= bytes;
|
|
12103
|
+
return settingsDigest(text2, 3);
|
|
12104
|
+
}
|
|
12082
12105
|
var sha = (data) => createHash10("sha256").update(data).digest("hex");
|
|
12083
12106
|
var ROOTS = NATIVE_SKILL_ROOTS;
|
|
12084
12107
|
function assertSafePath(path) {
|
|
@@ -12532,6 +12555,8 @@ function applyAgentIntegration(plan) {
|
|
|
12532
12555
|
parseManagedSkillPolicy(change.after);
|
|
12533
12556
|
const aliases = plan.rootAliases ?? [];
|
|
12534
12557
|
recheckRootAliases(aliases);
|
|
12558
|
+
for (const agent of plan.retainedReviewChecks?.agents ?? [])
|
|
12559
|
+
assertManagedAgentBridge(agent, { ...plan.retainedReviewChecks, dataDir: plan.dataDir });
|
|
12535
12560
|
for (const agent of plan.managedAgentChecks?.agents ?? [])
|
|
12536
12561
|
assertManagedAgentBridge(agent, { home: plan.managedAgentChecks.home, dataDir: plan.dataDir, projectDir: plan.managedAgentChecks.home });
|
|
12537
12562
|
for (const binding of plan.discoveryBefore ?? []) {
|
|
@@ -13545,13 +13570,17 @@ function unchanged2(before) {
|
|
|
13545
13570
|
const after = snapshot(before.file, before.privateFile, before.readOnlyPackage ? { readOnlyPackage: true } : {});
|
|
13546
13571
|
need4(sameStat(before.stat, after.stat) && after.sha256 === before.sha256, "INPUT_CHANGED");
|
|
13547
13572
|
}
|
|
13548
|
-
var
|
|
13573
|
+
var events = { UserPromptSubmit: ["userPromptSubmit", "user_prompt_submit"], SessionStart: ["sessionStart", "session_start"], SubagentStart: ["subagentStart", "subagent_start"] };
|
|
13574
|
+
var matchedEvents = { ...events, PreToolUse: ["preToolUse", "pre_tool_use"] };
|
|
13549
13575
|
var proofs = new WeakMap;
|
|
13576
|
+
var sha2 = (text2) => createHash15("sha256").update(text2).digest("hex");
|
|
13577
|
+
var proofs2 = new WeakMap;
|
|
13550
13578
|
function assertCodexHookDiscoveryRecovery(binding, witness) {
|
|
13551
|
-
const proof =
|
|
13579
|
+
const proof = proofs2.get(witness);
|
|
13552
13580
|
need4(!!proof && binding.agent === "codex" && witness.discoverySha256 === sha2(JSON.stringify(binding)), "RECONCILE_DISCOVERY_RECOVERY_CHANGED");
|
|
13553
13581
|
unchanged2(proof.before);
|
|
13554
13582
|
unchanged2(proof.current);
|
|
13583
|
+
proof.recheckAdditions();
|
|
13555
13584
|
}
|
|
13556
13585
|
function verifiesCodexHookDiscoverySource(binding, source, currentDigest, witness) {
|
|
13557
13586
|
if (!witness || source.path !== witness.configPath || source.hashMode !== "bytes")
|
|
@@ -13593,13 +13622,13 @@ function read2(path, changes) {
|
|
|
13593
13622
|
return readFileSync16(path, "utf8");
|
|
13594
13623
|
}
|
|
13595
13624
|
function projected(source, changes, budget = discoveryByteBudget()) {
|
|
13596
|
-
if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2") {
|
|
13625
|
+
if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3") {
|
|
13597
13626
|
if (source.format !== undefined || source.fields !== undefined || source.managedPlugins !== undefined || typeof source.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(source.sha256))
|
|
13598
13627
|
throw new Error("Claude settings witnesses require exact typed metadata");
|
|
13599
|
-
const current2 = (source.hashMode === "claude-settings-v2" ? captureClaudeSettingsV2 : captureClaudeSettings)(source.path, budget).sha256;
|
|
13628
|
+
const current2 = (source.hashMode === "claude-settings-v3" ? captureClaudeSettingsV3 : source.hashMode === "claude-settings-v2" ? captureClaudeSettingsV2 : captureClaudeSettings)(source.path, budget).sha256;
|
|
13600
13629
|
if (current2 !== source.sha256)
|
|
13601
13630
|
throw new Error(`Native discovery input changed; run skills hook install with a fresh discovery review: ${source.path}`);
|
|
13602
|
-
return changes?.has(source.path) ? (source.hashMode === "claude-settings-v2" ? hashClaudeSettingsReplacementV2 : hashClaudeSettingsReplacement)(changes.get(source.path), budget) : current2;
|
|
13631
|
+
return changes?.has(source.path) ? (source.hashMode === "claude-settings-v3" ? hashClaudeSettingsReplacementV3 : source.hashMode === "claude-settings-v2" ? hashClaudeSettingsReplacementV2 : hashClaudeSettingsReplacement)(changes.get(source.path), budget) : current2;
|
|
13603
13632
|
}
|
|
13604
13633
|
if (source.hashMode === "claude-marketplace-registry" || source.hashMode === "claude-marketplace-registry-v2") {
|
|
13605
13634
|
if (source.format !== undefined || source.fields !== undefined || source.managedPlugins !== undefined || typeof source.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(source.sha256) || changes?.has(source.path))
|
|
@@ -13661,7 +13690,7 @@ function verifyAgentDiscovery(binding, codexRecovery) {
|
|
|
13661
13690
|
safe4(root2);
|
|
13662
13691
|
}
|
|
13663
13692
|
function assertMarketplaceBinding(binding, source) {
|
|
13664
|
-
if ((source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2") && (binding.agent !== "claude" || binding.method !== "reviewed" || basename8(source.path) !== "settings.json"))
|
|
13693
|
+
if ((source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3") && (binding.agent !== "claude" || binding.method !== "reviewed" || basename8(source.path) !== "settings.json"))
|
|
13665
13694
|
throw new Error("Claude settings witnesses require explicit reviewed Claude configuration");
|
|
13666
13695
|
if ((source.hashMode === "claude-marketplace-registry" || source.hashMode === "claude-marketplace-registry-v2") && (binding.agent !== "claude" || binding.method !== "reviewed"))
|
|
13667
13696
|
throw new Error("Claude marketplace witnesses require explicit reviewed Claude discovery");
|
|
@@ -13670,7 +13699,10 @@ function rebindAgentDiscovery(binding, changes) {
|
|
|
13670
13699
|
const budget = discoveryByteBudget();
|
|
13671
13700
|
return { ...binding, sources: binding.sources.map((source) => {
|
|
13672
13701
|
assertMarketplaceBinding(binding, source);
|
|
13673
|
-
|
|
13702
|
+
const sha2562 = projected(source, changes, budget);
|
|
13703
|
+
if (!changes.has(source.path) && sha2562 !== source.sha256)
|
|
13704
|
+
throw new Error(`Native discovery input changed; run skills hook install with a fresh discovery review: ${source.path}`);
|
|
13705
|
+
return { ...source, sha256: sha2562 };
|
|
13674
13706
|
}) };
|
|
13675
13707
|
}
|
|
13676
13708
|
function assertProjectDiscovery(agent, directories, home, canonical4 = resolve14, reviewed) {
|
|
@@ -13782,6 +13814,19 @@ function resolveAgentDiscovery(options) {
|
|
|
13782
13814
|
}
|
|
13783
13815
|
}
|
|
13784
13816
|
}
|
|
13817
|
+
if (options.reviewed === undefined && options.retainedReview !== undefined) {
|
|
13818
|
+
const retained = options.retainedReview;
|
|
13819
|
+
if (retained.agent !== agent || retained.method !== "reviewed")
|
|
13820
|
+
unresolved("invalid retained review");
|
|
13821
|
+
verifyAgentDiscovery(retained);
|
|
13822
|
+
if (!retained.sources.some((source) => source.path === canonical4(configPath) && source.format === undefined && source.fields === undefined))
|
|
13823
|
+
unresolved("retained review is missing the full agent configuration source");
|
|
13824
|
+
if (retained.sources.some((source) => (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3") && source.path !== canonical4(configPath)))
|
|
13825
|
+
unresolved("retained settings witness names another configuration source");
|
|
13826
|
+
if (sources.some((source) => !retained.sources.some((saved) => isDeepStrictEqual3(saved, source))) || [...roots].some((root2) => !retained.roots.includes(root2)) || agent === "gemini" && JSON.stringify(retained.builtinNames) !== JSON.stringify(builtinNames))
|
|
13827
|
+
unresolved("retained review is missing current runtime discovery coverage");
|
|
13828
|
+
return retained;
|
|
13829
|
+
}
|
|
13785
13830
|
if (options.reviewed && (options.reviewed.version !== 1 || !Array.isArray(options.reviewed.agents) || options.reviewed.agents.some((item) => !item || typeof item !== "object") || new Set(options.reviewed.agents.map((item) => item.agent)).size !== options.reviewed.agents.length))
|
|
13786
13831
|
throw new Error("Invalid --discovery-inputs version or agents");
|
|
13787
13832
|
const review = options.reviewed?.agents.find((item) => item.agent === agent);
|
|
@@ -13792,7 +13837,7 @@ function resolveAgentDiscovery(options) {
|
|
|
13792
13837
|
if (review.sources.some((source) => source.format !== undefined || source.fields !== undefined))
|
|
13793
13838
|
throw new Error("Explicit discovery reviews require full source-file hashes");
|
|
13794
13839
|
verifyAgentDiscovery(supplied);
|
|
13795
|
-
if (review.sources.some((source) => (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2") && source.path !== canonical4(configPath)))
|
|
13840
|
+
if (review.sources.some((source) => (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3") && source.path !== canonical4(configPath)))
|
|
13796
13841
|
throw new Error("Semantic settings witnesses must name the configured Claude configuration source");
|
|
13797
13842
|
if (!review.sources.some((source) => source.path === canonical4(configPath)))
|
|
13798
13843
|
throw new Error("Discovery review must include the agent configuration source");
|
|
@@ -34302,8 +34347,8 @@ var SESSION_RECONCILIATION_PLAN_TTL_MS = 5 * 60 * 1000;
|
|
|
34302
34347
|
// src/lib/claude-skills-coordination.ts
|
|
34303
34348
|
function coordinateClaudeSettings(before, replacement, options) {
|
|
34304
34349
|
const prior = JSON.parse(before ?? "{}"), next = JSON.parse(replacement);
|
|
34305
|
-
const
|
|
34306
|
-
const changed = [...
|
|
34350
|
+
const events2 = new Set([...Object.keys(prior.hooks ?? {}), ...Object.keys(next.hooks ?? {})]);
|
|
34351
|
+
const changed = [...events2].filter((event) => JSON.stringify(prior.hooks?.[event]) !== JSON.stringify(next.hooks?.[event]));
|
|
34307
34352
|
const selected = changed.length ? changed : ["PreToolUse"];
|
|
34308
34353
|
const plan = planClaudeHookEventsUpdate({
|
|
34309
34354
|
...options,
|
|
@@ -34389,9 +34434,9 @@ function getTargetSettingsDir(target) {
|
|
|
34389
34434
|
}
|
|
34390
34435
|
function getGlobalSettingsPathOverride(target) {
|
|
34391
34436
|
if (target === "codex")
|
|
34392
|
-
return process.env.HASNA_HOOKS_CODEX_SETTINGS_PATH;
|
|
34437
|
+
return process.env.HASNA_HOOKS_CODEX_SETTINGS_PATH ?? (process.env.CODEX_HOME ? resolve7(process.env.CODEX_HOME, "hooks.json") : undefined);
|
|
34393
34438
|
if (target === "claude")
|
|
34394
|
-
return process.env.HASNA_HOOKS_CLAUDE_SETTINGS_PATH;
|
|
34439
|
+
return process.env.HASNA_HOOKS_CLAUDE_SETTINGS_PATH ?? (process.env.CLAUDE_CONFIG_DIR ? resolve7(process.env.CLAUDE_CONFIG_DIR, "settings.json") : undefined);
|
|
34395
34440
|
if (target === "gemini")
|
|
34396
34441
|
return process.env.HASNA_HOOKS_GEMINI_SETTINGS_PATH;
|
|
34397
34442
|
return;
|
|
@@ -34435,7 +34480,7 @@ function readSettings2(scope = "global", target = "claude") {
|
|
|
34435
34480
|
return value;
|
|
34436
34481
|
}
|
|
34437
34482
|
} catch (error61) {
|
|
34438
|
-
console.warn(
|
|
34483
|
+
console.warn("[hooks] Cannot read hook settings; repair the selected file before installation.");
|
|
34439
34484
|
}
|
|
34440
34485
|
const empty = {};
|
|
34441
34486
|
if (!existsSync10(path))
|
|
@@ -34447,10 +34492,16 @@ function settingsIdentity(path) {
|
|
|
34447
34492
|
return realpathSync5(path);
|
|
34448
34493
|
return existsSync10(dirname7(path)) ? join10(realpathSync5(dirname7(path)), basename3(path)) : resolve7(path);
|
|
34449
34494
|
}
|
|
34450
|
-
function writeSettings(settings, scope = "global", target = "claude") {
|
|
34495
|
+
function writeSettings(settings, scope = "global", target = "claude", expectedSettingsSha256) {
|
|
34451
34496
|
const path = getSettingsPath(scope, target);
|
|
34452
34497
|
if (target === "codex")
|
|
34453
|
-
return writeCodexSettings(path, settings);
|
|
34498
|
+
return writeCodexSettings(path, settings, expectedSettingsSha256);
|
|
34499
|
+
const captured = settingsPreimages.get(settings);
|
|
34500
|
+
if (expectedSettingsSha256 !== undefined) {
|
|
34501
|
+
if (!captured || captured.path !== path)
|
|
34502
|
+
throw new Error("Hook settings require a valid captured preimage.");
|
|
34503
|
+
assertSettingsPreimage(captured.text, expectedSettingsSha256);
|
|
34504
|
+
}
|
|
34454
34505
|
if (target === "claude" && settingsIdentity(path) === settingsIdentity(join10(homedir5(), ".claude/settings.json"))) {
|
|
34455
34506
|
const before = settingsPreimages.get(settings);
|
|
34456
34507
|
if (!before || before.path !== path)
|
|
@@ -34462,6 +34513,8 @@ function writeSettings(settings, scope = "global", target = "claude") {
|
|
|
34462
34513
|
`, { home: homedir5() }))
|
|
34463
34514
|
return;
|
|
34464
34515
|
}
|
|
34516
|
+
if (expectedSettingsSha256 !== undefined)
|
|
34517
|
+
return writeHookSettingsPreimage(path, settings, captured.text);
|
|
34465
34518
|
const dir = dirname7(path);
|
|
34466
34519
|
if (!existsSync10(dir)) {
|
|
34467
34520
|
mkdirSync6(dir, { recursive: true });
|
|
@@ -34583,7 +34636,7 @@ function detectConflict(name, scope, target) {
|
|
|
34583
34636
|
const meta3 = isNativeSafetyName(name) ? getHook(name) : resolveHookMeta(name);
|
|
34584
34637
|
if (!meta3 || !meta3.matcher)
|
|
34585
34638
|
return;
|
|
34586
|
-
const
|
|
34639
|
+
const events2 = new Set(getHookEvents(meta3));
|
|
34587
34640
|
const registered = getRegisteredHooksForTarget(scope, target);
|
|
34588
34641
|
for (const existingName of registered) {
|
|
34589
34642
|
if (existingName === name)
|
|
@@ -34591,7 +34644,7 @@ function detectConflict(name, scope, target) {
|
|
|
34591
34644
|
const existing = getHook(existingName);
|
|
34592
34645
|
if (!existing || !existing.matcher)
|
|
34593
34646
|
continue;
|
|
34594
|
-
if (!getHookEvents(existing).some((event) =>
|
|
34647
|
+
if (!getHookEvents(existing).some((event) => events2.has(event)))
|
|
34595
34648
|
continue;
|
|
34596
34649
|
if (matchersOverlap(meta3.matcher.toLowerCase(), existing.matcher.toLowerCase())) {
|
|
34597
34650
|
return `conflicts with '${existingName}' (same event ${meta3.event}, overlapping matcher '${existing.matcher}')`;
|
|
@@ -34619,7 +34672,7 @@ function detectRewriteConflict(name, scope, target, lookup = getHook) {
|
|
|
34619
34672
|
}
|
|
34620
34673
|
return;
|
|
34621
34674
|
}
|
|
34622
|
-
function installForTarget(name, scope, overwrite, target, profile, codewithMode = "fragment", codewithConfigPath, mementos) {
|
|
34675
|
+
function installForTarget(name, scope, overwrite, target, profile, codewithMode = "fragment", codewithConfigPath, mementos, expectedSettingsSha256) {
|
|
34623
34676
|
const shortName = shortHookName(name);
|
|
34624
34677
|
if (isNativeSafetyName(shortName) && (profile || target === "gemini" || target === "codewith" && codewithMode === "write")) {
|
|
34625
34678
|
return { hook: shortName, success: false, error: "Bundled native safety guards support Claude/Codex and managed Codewith fragments, without registry profiles.", target };
|
|
@@ -34686,7 +34739,7 @@ function installForTarget(name, scope, overwrite, target, profile, codewithMode
|
|
|
34686
34739
|
if (rewriteConflict) {
|
|
34687
34740
|
return { hook: shortName, success: false, error: `Refused: ${rewriteConflict}`, scope, target };
|
|
34688
34741
|
}
|
|
34689
|
-
registerHook(shortName, scope, target, profile, mementos);
|
|
34742
|
+
registerHook(shortName, scope, target, profile, mementos, (settings, selectedScope, selectedTarget) => writeSettings(settings, selectedScope, selectedTarget, expectedSettingsSha256));
|
|
34690
34743
|
return {
|
|
34691
34744
|
hook: shortName,
|
|
34692
34745
|
success: true,
|
|
@@ -34705,7 +34758,10 @@ function installForTarget(name, scope, overwrite, target, profile, codewithMode
|
|
|
34705
34758
|
}
|
|
34706
34759
|
}
|
|
34707
34760
|
function installHook(name, options = {}) {
|
|
34708
|
-
const { scope = "global", overwrite = false, target = "claude", profile, codewithMode = "fragment", codewithConfigPath, mementos } = options;
|
|
34761
|
+
const { scope = "global", overwrite = false, target = "claude", profile, codewithMode = "fragment", codewithConfigPath, mementos, expectedSettingsSha256 } = options;
|
|
34762
|
+
if (expectedSettingsSha256 !== undefined && (!/^(?:absent|[a-f0-9]{64})$/.test(expectedSettingsSha256) || !isNativeSafetyName(shortHookName(name)) || target !== "codex" && target !== "claude")) {
|
|
34763
|
+
return { hook: name, success: false, target, error: "An exact settings predecessor requires a Claude or Codex native safety guard." };
|
|
34764
|
+
}
|
|
34709
34765
|
try {
|
|
34710
34766
|
validateMementosRegistration(name, mementos);
|
|
34711
34767
|
} catch (error61) {
|
|
@@ -34746,9 +34802,9 @@ function installHook(name, options = {}) {
|
|
|
34746
34802
|
}
|
|
34747
34803
|
return { ...claudeResult, target: "all" };
|
|
34748
34804
|
}
|
|
34749
|
-
return installForTarget(name, scope, overwrite, target, profile, codewithMode, codewithConfigPath, mementos);
|
|
34805
|
+
return installForTarget(name, scope, overwrite, target, profile, codewithMode, codewithConfigPath, mementos, expectedSettingsSha256);
|
|
34750
34806
|
}
|
|
34751
|
-
function registerHook(name, scope = "global", target = "claude", profile, mementos) {
|
|
34807
|
+
function registerHook(name, scope = "global", target = "claude", profile, mementos, write = writeSettings) {
|
|
34752
34808
|
const meta3 = isNativeSafetyName(name) ? getHook(name) : resolveHookMeta(name);
|
|
34753
34809
|
if (!meta3)
|
|
34754
34810
|
return;
|
|
@@ -34777,7 +34833,7 @@ function registerHook(name, scope = "global", target = "claude", profile, mement
|
|
|
34777
34833
|
}
|
|
34778
34834
|
const retained = name === "mementos-context" && mementos === undefined ? retainedMementosRegistration(Object.values(settings.hooks).flatMap((groups) => Array.isArray(groups) ? groups.flatMap((group) => Array.isArray(group.hooks) ? group.hooks.map((hook) => hook.command).filter((command) => typeof command === "string") : []) : [])) : undefined;
|
|
34779
34835
|
const hookCommand = isNativeSafetyName(name) ? installedNativeSafetyCommand(name) : (profile ? `hooks run ${name} --profile ${profile}` : `hooks run ${name}`) + mementosCommandSuffix(mementos ?? retained);
|
|
34780
|
-
if (target === "codex") {
|
|
34836
|
+
if (target === "codex" || target === "claude" && isNativeSafetyName(name)) {
|
|
34781
34837
|
const positions = [];
|
|
34782
34838
|
for (const [event, groups] of Object.entries(settings.hooks)) {
|
|
34783
34839
|
for (const entry of groups) {
|
|
@@ -34803,7 +34859,7 @@ function registerHook(name, scope = "global", target = "claude", profile, mement
|
|
|
34803
34859
|
if (typeof meta3.timeoutSeconds === "number" && meta3.timeoutSeconds > 0)
|
|
34804
34860
|
entry.hooks[index].timeout = meta3.timeoutSeconds;
|
|
34805
34861
|
}
|
|
34806
|
-
|
|
34862
|
+
write(settings, scope, target);
|
|
34807
34863
|
return;
|
|
34808
34864
|
}
|
|
34809
34865
|
}
|
|
@@ -34823,7 +34879,7 @@ function registerHook(name, scope = "global", target = "claude", profile, mement
|
|
|
34823
34879
|
}
|
|
34824
34880
|
settings.hooks[eventKey].push(entry);
|
|
34825
34881
|
}
|
|
34826
|
-
|
|
34882
|
+
write(settings, scope, target);
|
|
34827
34883
|
}
|
|
34828
34884
|
function removeHookFromAllEvents(settings, name) {
|
|
34829
34885
|
if (!settings.hooks)
|