@hasna/hooks 0.10.7 → 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 +39 -1
- package/bin/hooks-mcp.js +52 -19
- package/bin/index.js +239 -80
- package/bin/native-safety-entry.js +196 -15
- package/dist/index.js +52 -19
- 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
|
@@ -1179,6 +1179,126 @@ var OUTPUT_REDIRECT = /(?:\d+|&)?(?:>>|>\||>&|<>|>)\s*([^\s;&|<>()\x60]*)/g;
|
|
|
1179
1179
|
function withoutFdDuplication(segment) {
|
|
1180
1180
|
return segment.replace(FD_DUPLICATION, " ");
|
|
1181
1181
|
}
|
|
1182
|
+
function pipelineStages(segment) {
|
|
1183
|
+
const stages = [];
|
|
1184
|
+
let quote = null;
|
|
1185
|
+
let parens = 0;
|
|
1186
|
+
let braces = 0;
|
|
1187
|
+
let backtick = false;
|
|
1188
|
+
let start = 0;
|
|
1189
|
+
for (let i = 0;i < segment.length; i++) {
|
|
1190
|
+
const ch = segment[i];
|
|
1191
|
+
if (quote === "'") {
|
|
1192
|
+
if (ch === "'")
|
|
1193
|
+
quote = null;
|
|
1194
|
+
continue;
|
|
1195
|
+
}
|
|
1196
|
+
if (ch === "\\") {
|
|
1197
|
+
i++;
|
|
1198
|
+
continue;
|
|
1199
|
+
}
|
|
1200
|
+
if (ch === "(") {
|
|
1201
|
+
parens++;
|
|
1202
|
+
continue;
|
|
1203
|
+
}
|
|
1204
|
+
if (ch === ")") {
|
|
1205
|
+
if (--parens < 0)
|
|
1206
|
+
return [segment];
|
|
1207
|
+
continue;
|
|
1208
|
+
}
|
|
1209
|
+
if (ch === "$" && segment[i + 1] === "{") {
|
|
1210
|
+
braces++;
|
|
1211
|
+
i++;
|
|
1212
|
+
continue;
|
|
1213
|
+
}
|
|
1214
|
+
if (ch === "}" && braces > 0) {
|
|
1215
|
+
braces--;
|
|
1216
|
+
continue;
|
|
1217
|
+
}
|
|
1218
|
+
if (ch === "`") {
|
|
1219
|
+
backtick = !backtick;
|
|
1220
|
+
continue;
|
|
1221
|
+
}
|
|
1222
|
+
if (quote === '"') {
|
|
1223
|
+
if (ch === '"')
|
|
1224
|
+
quote = null;
|
|
1225
|
+
continue;
|
|
1226
|
+
}
|
|
1227
|
+
if (ch === "'" || ch === '"') {
|
|
1228
|
+
quote = ch;
|
|
1229
|
+
continue;
|
|
1230
|
+
}
|
|
1231
|
+
if (ch !== "|" || parens > 0 || braces > 0 || backtick || segment[i - 1] === ">")
|
|
1232
|
+
continue;
|
|
1233
|
+
stages.push(segment.slice(start, i));
|
|
1234
|
+
if (segment[i + 1] === "&")
|
|
1235
|
+
i++;
|
|
1236
|
+
start = i + 1;
|
|
1237
|
+
}
|
|
1238
|
+
if (quote || parens !== 0 || braces !== 0 || backtick)
|
|
1239
|
+
return [segment];
|
|
1240
|
+
stages.push(segment.slice(start));
|
|
1241
|
+
return stages;
|
|
1242
|
+
}
|
|
1243
|
+
function shellWords(command) {
|
|
1244
|
+
const words = [];
|
|
1245
|
+
let text = "";
|
|
1246
|
+
let raw = "";
|
|
1247
|
+
let quote = null;
|
|
1248
|
+
let inWord = false;
|
|
1249
|
+
const flush = () => {
|
|
1250
|
+
if (!inWord)
|
|
1251
|
+
return;
|
|
1252
|
+
const body = raw.replace(/^"?\$\{?HOME\}?"?(?=\/|$)/, "");
|
|
1253
|
+
const literalTilde = text.startsWith("~") && !/^~(?:\/|$)/.test(raw);
|
|
1254
|
+
words.push({ text: literalTilde ? `./${text}` : text, opaque: /[$\x60*?[]/.test(body) || /[()]/.test(body) });
|
|
1255
|
+
text = "";
|
|
1256
|
+
raw = "";
|
|
1257
|
+
inWord = false;
|
|
1258
|
+
};
|
|
1259
|
+
for (let i = 0;i < command.length; i++) {
|
|
1260
|
+
const ch = command[i];
|
|
1261
|
+
if (quote === "'") {
|
|
1262
|
+
if (ch === "'")
|
|
1263
|
+
quote = null;
|
|
1264
|
+
else
|
|
1265
|
+
text += ch;
|
|
1266
|
+
raw += ch;
|
|
1267
|
+
continue;
|
|
1268
|
+
}
|
|
1269
|
+
if (ch === "\\" && i + 1 < command.length) {
|
|
1270
|
+
const next = command[i + 1];
|
|
1271
|
+
text += quote === '"' && !/[$\x60"\\\n]/.test(next) ? ch + next : next;
|
|
1272
|
+
raw += ch + next;
|
|
1273
|
+
inWord = true;
|
|
1274
|
+
i++;
|
|
1275
|
+
continue;
|
|
1276
|
+
}
|
|
1277
|
+
if (quote === '"') {
|
|
1278
|
+
if (ch === '"')
|
|
1279
|
+
quote = null;
|
|
1280
|
+
else
|
|
1281
|
+
text += ch;
|
|
1282
|
+
raw += ch;
|
|
1283
|
+
continue;
|
|
1284
|
+
}
|
|
1285
|
+
if (/\s/.test(ch)) {
|
|
1286
|
+
flush();
|
|
1287
|
+
continue;
|
|
1288
|
+
}
|
|
1289
|
+
inWord = true;
|
|
1290
|
+
raw += ch;
|
|
1291
|
+
if (ch === "'" || ch === '"')
|
|
1292
|
+
quote = ch;
|
|
1293
|
+
else
|
|
1294
|
+
text += ch;
|
|
1295
|
+
}
|
|
1296
|
+
if (quote) {
|
|
1297
|
+
raw += "$";
|
|
1298
|
+
}
|
|
1299
|
+
flush();
|
|
1300
|
+
return words;
|
|
1301
|
+
}
|
|
1182
1302
|
function segmentOperation(segment) {
|
|
1183
1303
|
const trimmed = segment.trim();
|
|
1184
1304
|
if (!trimmed)
|
|
@@ -1281,7 +1401,6 @@ function bashTargets(command, home, cwd) {
|
|
|
1281
1401
|
for (let i = 0;i < opens; i++)
|
|
1282
1402
|
subshells.push({ current: [...current], reachable: new Set(reachable), certain });
|
|
1283
1403
|
const segment = unwrapSegment(rawSegment);
|
|
1284
|
-
const op = segmentOperation(segment);
|
|
1285
1404
|
const plainCd = segment.match(/^cd(?:\s+(?:-[A-Za-z@]+|--))*(?:\s+([^\s;&|<>()\x60]+))?$/);
|
|
1286
1405
|
const embeddedCd = plainCd ? null : segment.match(/(?:^|\s)cd(?:\s+(?:-[A-Za-z]+|--))*\s+([^\s;&|<>()\x60]+)/);
|
|
1287
1406
|
if (embeddedCd) {
|
|
@@ -1294,28 +1413,90 @@ function bashTargets(command, home, cwd) {
|
|
|
1294
1413
|
const suffix = roots.map((root) => regexEscape(root.slice(home.length + 1))).join("|");
|
|
1295
1414
|
const prefixRe = new RegExp(`(?:~|\\$HOME"*|\\$\\{HOME\\}"*|${homeLiteral}"*)/(?:${suffix})`);
|
|
1296
1415
|
const re = new RegExp(`(${prefixRe.source})([^\\s"';&|<>()\`]*|$)`, "g");
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1416
|
+
const explicitTargets = (text) => {
|
|
1417
|
+
const found = [];
|
|
1418
|
+
let m;
|
|
1419
|
+
re.lastIndex = 0;
|
|
1420
|
+
while ((m = re.exec(text)) !== null)
|
|
1421
|
+
found.push(normalize2(expandHomeSpelling2(m[0], home)).replace(/\/+$/, ""));
|
|
1422
|
+
return found;
|
|
1423
|
+
};
|
|
1424
|
+
const stages = pipelineStages(segment).map((stage) => {
|
|
1305
1425
|
const operands = [];
|
|
1306
|
-
|
|
1426
|
+
let opaqueRedirect = false;
|
|
1427
|
+
const commandPart = withoutFdDuplication(stage).replace(OUTPUT_REDIRECT, (_match, operand) => {
|
|
1428
|
+
if (!operand || /[$\x60]/.test(operand))
|
|
1429
|
+
opaqueRedirect = true;
|
|
1307
1430
|
if (operand)
|
|
1308
1431
|
operands.push(operand);
|
|
1309
1432
|
return " ";
|
|
1310
1433
|
});
|
|
1311
1434
|
const commandOp = segmentOperation(commandPart);
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1435
|
+
return { commandPart, operands, op: opaqueRedirect && commandOp === "read" ? "write" : commandOp };
|
|
1436
|
+
});
|
|
1437
|
+
const pipelineOp = stages.some((stage) => stage.op === "delete") ? "delete" : stages.some((stage) => stage.op === "write") ? "write" : "read";
|
|
1438
|
+
const bases = current.filter(underRoot);
|
|
1439
|
+
for (const stage of stages) {
|
|
1440
|
+
let foundExplicit = false;
|
|
1441
|
+
for (const path of explicitTargets(stage.commandPart)) {
|
|
1442
|
+
foundExplicit = true;
|
|
1443
|
+
targets.push({ path, op: pipelineOp });
|
|
1444
|
+
}
|
|
1445
|
+
for (const operand of stage.operands) {
|
|
1446
|
+
const explicit = explicitTargets(operand);
|
|
1447
|
+
for (const path of explicit)
|
|
1448
|
+
targets.push({ path, op: "write" });
|
|
1449
|
+
if (explicit.length > 0)
|
|
1450
|
+
continue;
|
|
1451
|
+
for (const base of bases) {
|
|
1315
1452
|
targets.push({ path: normalize2(resolve3(base, expandHomeSpelling2(operand, home))), op: "write" });
|
|
1316
1453
|
}
|
|
1317
|
-
|
|
1318
|
-
|
|
1454
|
+
}
|
|
1455
|
+
if (foundExplicit || stage.op === "read")
|
|
1456
|
+
continue;
|
|
1457
|
+
const words = shellWords(stage.commandPart);
|
|
1458
|
+
let endOfOptions = false;
|
|
1459
|
+
const nonFlag = words.slice(1).flatMap((word) => {
|
|
1460
|
+
if (!endOfOptions && word.text === "--") {
|
|
1461
|
+
endOfOptions = true;
|
|
1462
|
+
return [];
|
|
1463
|
+
}
|
|
1464
|
+
if (!endOfOptions && word.text.startsWith("-"))
|
|
1465
|
+
return [];
|
|
1466
|
+
endOfOptions = true;
|
|
1467
|
+
return [word.opaque ? "." : word.text];
|
|
1468
|
+
});
|
|
1469
|
+
const everyOperand = stage.op === "delete" || /(?:^|\s)(?:mkdir|mkfile|touch|tee|truncate|install|mv)(?:\s|$)/.test(stage.commandPart) || /(?:^|\s)sed(?:\s|$)/.test(stage.commandPart) && /(?:^|\s)(?:-i\S*|--in-place\S*)(?:\s|$)/.test(stage.commandPart);
|
|
1470
|
+
const operandTargets = [];
|
|
1471
|
+
if (everyOperand) {
|
|
1472
|
+
for (const operand of nonFlag.length > 0 ? nonFlag : ["."])
|
|
1473
|
+
operandTargets.push({ operand, op: stage.op });
|
|
1474
|
+
} else {
|
|
1475
|
+
const relMatch = stage.commandPart.match(REL_OPERAND);
|
|
1476
|
+
const last = words.at(-1);
|
|
1477
|
+
operandTargets.push({ operand: relMatch ? relMatch[1] : !last || last.opaque ? "." : last.text, op: stage.op });
|
|
1478
|
+
if (/(?:^|\s)(?:cp|ln|install|mv)(?:\s|$)/.test(stage.commandPart)) {
|
|
1479
|
+
words.forEach((word, index2) => {
|
|
1480
|
+
let value;
|
|
1481
|
+
const short = word.text.match(/^-[A-Za-z]*?t(.*)$/);
|
|
1482
|
+
const long = word.text.match(/^(--t[a-z-]*)(?:=(.*))?$/);
|
|
1483
|
+
if (short && !word.text.startsWith("--")) {
|
|
1484
|
+
value = short[1] ? { text: short[1], opaque: word.opaque } : words[index2 + 1];
|
|
1485
|
+
} else if (long && "--target-directory".startsWith(long[1])) {
|
|
1486
|
+
value = long[2] !== undefined ? { text: long[2], opaque: word.opaque } : words[index2 + 1];
|
|
1487
|
+
}
|
|
1488
|
+
if (value)
|
|
1489
|
+
operandTargets.push({ operand: value.opaque || !value.text ? "." : value.text, op: stage.op });
|
|
1490
|
+
});
|
|
1491
|
+
}
|
|
1492
|
+
if (/(?:^|\s)(?:python3?|node|bun)\b[^;&|]*\s+-[ce](?:\s|$)/.test(stage.commandPart)) {
|
|
1493
|
+
operandTargets.push({ operand: ".", op: stage.op });
|
|
1494
|
+
}
|
|
1495
|
+
}
|
|
1496
|
+
for (const base of bases) {
|
|
1497
|
+
for (const { operand, op } of operandTargets) {
|
|
1498
|
+
targets.push({ path: normalize2(resolve3(base, expandHomeSpelling2(operand, home))), op });
|
|
1499
|
+
}
|
|
1319
1500
|
}
|
|
1320
1501
|
}
|
|
1321
1502
|
if (plainCd) {
|
package/dist/index.js
CHANGED
|
@@ -7941,7 +7941,7 @@ function writeHookSettingsPreimage(path, settings, before) {
|
|
|
7941
7941
|
// src/lib/claude-skills-coordination.ts
|
|
7942
7942
|
import { createHash as createHash5 } from "crypto";
|
|
7943
7943
|
|
|
7944
|
-
// ../../node_modules/.bun/@hasna+skills@0.9.
|
|
7944
|
+
// ../../node_modules/.bun/@hasna+skills@0.9.17+a00d3064559565bb/node_modules/@hasna/skills/dist/index.js
|
|
7945
7945
|
import { existsSync as existsSync9 } from "fs";
|
|
7946
7946
|
import { homedir as homedir4 } from "os";
|
|
7947
7947
|
import { join as join8, resolve as resolve3 } from "path";
|
|
@@ -7954,6 +7954,7 @@ import { basename as basename2, isAbsolute as isAbsolute22, resolve as resolve22
|
|
|
7954
7954
|
import { createHash as createHash16 } from "crypto";
|
|
7955
7955
|
import { lstatSync as lstatSync20, readFileSync as readFileSync16, readdirSync as readdirSync9, realpathSync as realpathSync4 } from "fs";
|
|
7956
7956
|
import { basename as basename8, dirname as dirname16, isAbsolute as isAbsolute14, join as join26, resolve as resolve14, sep as sep5 } from "path";
|
|
7957
|
+
import { isDeepStrictEqual as isDeepStrictEqual3 } from "util";
|
|
7957
7958
|
import { dirname as dirname32, isAbsolute as isAbsolute3, join as join72, resolve as resolve32 } from "path";
|
|
7958
7959
|
import { lstatSync as lstatSync3, readFileSync as readFileSync6, opendirSync } from "fs";
|
|
7959
7960
|
import { createHash as createHash22 } from "crypto";
|
|
@@ -15434,8 +15435,8 @@ function assertAgentPolicyCollections(policy) {
|
|
|
15434
15435
|
text(source.path);
|
|
15435
15436
|
requireBound(source.sha256 === null || typeof source.sha256 === "string" && /^[a-f0-9]{64}$/.test(source.sha256));
|
|
15436
15437
|
if (source.hashMode !== undefined)
|
|
15437
|
-
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));
|
|
15438
|
-
if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2")
|
|
15438
|
+
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));
|
|
15439
|
+
if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3")
|
|
15439
15440
|
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");
|
|
15440
15441
|
if (source.hashMode === "claude-marketplace-registry" || source.hashMode === "claude-marketplace-registry-v2")
|
|
15441
15442
|
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");
|
|
@@ -15930,7 +15931,7 @@ function hashDiscoveryPathFile(path, budget, changes) {
|
|
|
15930
15931
|
}
|
|
15931
15932
|
var package_default = {
|
|
15932
15933
|
name: "@hasna/skills",
|
|
15933
|
-
version: "0.9.
|
|
15934
|
+
version: "0.9.17",
|
|
15934
15935
|
description: "Skills library for AI coding agents",
|
|
15935
15936
|
type: "module",
|
|
15936
15937
|
bin: {
|
|
@@ -16785,7 +16786,7 @@ function assertClaudeHookEventsReplacement(before, replacement, events) {
|
|
|
16785
16786
|
need2(JSON.stringify(withoutOwnedEvents(original)) === JSON.stringify(withoutOwnedEvents(next)), `${label} update changed settings outside hooks.${label}`);
|
|
16786
16787
|
}
|
|
16787
16788
|
var PERSISTED_EFFORT_LEVELS = new Set(["low", "medium", "high", "xhigh"]);
|
|
16788
|
-
function inferencePreference(key, child) {
|
|
16789
|
+
function inferencePreference(key, child, version) {
|
|
16789
16790
|
if (key === "skipDangerousModePermissionPrompt") {
|
|
16790
16791
|
need2(child.kind === "literal" && (child.value === "true" || child.value === "false"), "contains an invalid dialog acknowledgment");
|
|
16791
16792
|
return false;
|
|
@@ -16798,7 +16799,7 @@ function inferencePreference(key, child) {
|
|
|
16798
16799
|
return false;
|
|
16799
16800
|
need2(child.kind === "object", "requires an object of model preferences");
|
|
16800
16801
|
child.entries = child.entries.filter(([model, preferences]) => {
|
|
16801
|
-
if (!BUILTIN_MODEL_SELECTIONS.has(model) || preferences.kind !== "object" || preferences.entries.length !== 1 || preferences.entries[0][0] !== "effortLevel")
|
|
16802
|
+
if (version === 2 && !BUILTIN_MODEL_SELECTIONS.has(model) || preferences.kind !== "object" || preferences.entries.length !== 1 || preferences.entries[0][0] !== "effortLevel")
|
|
16802
16803
|
return true;
|
|
16803
16804
|
const effort = preferences.entries[0][1];
|
|
16804
16805
|
need2(effort.kind === "string" && PERSISTED_EFFORT_LEVELS.has(effort.value), "contains an invalid persisted model effort level");
|
|
@@ -16809,7 +16810,7 @@ function inferencePreference(key, child) {
|
|
|
16809
16810
|
function settingsDigest(text2, version = 1) {
|
|
16810
16811
|
const value = parse3(text2);
|
|
16811
16812
|
value.entries = value.entries.filter(([key, child]) => {
|
|
16812
|
-
if (version
|
|
16813
|
+
if (version !== 1 && inferencePreference(key, child, version))
|
|
16813
16814
|
return false;
|
|
16814
16815
|
if (key === "model") {
|
|
16815
16816
|
need2(child.kind === "string", "contains an invalid model selection");
|
|
@@ -16847,6 +16848,16 @@ function hashClaudeSettingsReplacementV2(text2, budget) {
|
|
|
16847
16848
|
budget.remaining -= bytes;
|
|
16848
16849
|
return settingsDigest(text2, 2);
|
|
16849
16850
|
}
|
|
16851
|
+
function captureClaudeSettingsV3(path, budget = { remaining: 256 * 1024 * 1024 }) {
|
|
16852
|
+
return { path, hashMode: "claude-settings-v3", sha256: settingsDigest(readSettings(path, budget), 3) };
|
|
16853
|
+
}
|
|
16854
|
+
function hashClaudeSettingsReplacementV3(text2, budget) {
|
|
16855
|
+
need2(typeof text2 === "string", "requires settings text");
|
|
16856
|
+
const bytes = Buffer.byteLength(text2);
|
|
16857
|
+
need2(bytes <= CLAUDE_SETTINGS_WITNESS_LIMITS.bytes && Number.isSafeInteger(budget.remaining) && budget.remaining >= bytes, "exceeds its byte limit");
|
|
16858
|
+
budget.remaining -= bytes;
|
|
16859
|
+
return settingsDigest(text2, 3);
|
|
16860
|
+
}
|
|
16850
16861
|
var sha = (data) => createHash10("sha256").update(data).digest("hex");
|
|
16851
16862
|
var ROOTS = NATIVE_SKILL_ROOTS;
|
|
16852
16863
|
function assertSafePath(path) {
|
|
@@ -17300,6 +17311,8 @@ function applyAgentIntegration(plan) {
|
|
|
17300
17311
|
parseManagedSkillPolicy(change.after);
|
|
17301
17312
|
const aliases = plan.rootAliases ?? [];
|
|
17302
17313
|
recheckRootAliases(aliases);
|
|
17314
|
+
for (const agent of plan.retainedReviewChecks?.agents ?? [])
|
|
17315
|
+
assertManagedAgentBridge(agent, { ...plan.retainedReviewChecks, dataDir: plan.dataDir });
|
|
17303
17316
|
for (const agent of plan.managedAgentChecks?.agents ?? [])
|
|
17304
17317
|
assertManagedAgentBridge(agent, { home: plan.managedAgentChecks.home, dataDir: plan.dataDir, projectDir: plan.managedAgentChecks.home });
|
|
17305
17318
|
for (const binding of plan.discoveryBefore ?? []) {
|
|
@@ -18313,13 +18326,17 @@ function unchanged2(before) {
|
|
|
18313
18326
|
const after = snapshot(before.file, before.privateFile, before.readOnlyPackage ? { readOnlyPackage: true } : {});
|
|
18314
18327
|
need4(sameStat(before.stat, after.stat) && after.sha256 === before.sha256, "INPUT_CHANGED");
|
|
18315
18328
|
}
|
|
18316
|
-
var
|
|
18329
|
+
var events = { UserPromptSubmit: ["userPromptSubmit", "user_prompt_submit"], SessionStart: ["sessionStart", "session_start"], SubagentStart: ["subagentStart", "subagent_start"] };
|
|
18330
|
+
var matchedEvents = { ...events, PreToolUse: ["preToolUse", "pre_tool_use"] };
|
|
18317
18331
|
var proofs = new WeakMap;
|
|
18332
|
+
var sha2 = (text2) => createHash15("sha256").update(text2).digest("hex");
|
|
18333
|
+
var proofs2 = new WeakMap;
|
|
18318
18334
|
function assertCodexHookDiscoveryRecovery(binding, witness) {
|
|
18319
|
-
const proof =
|
|
18335
|
+
const proof = proofs2.get(witness);
|
|
18320
18336
|
need4(!!proof && binding.agent === "codex" && witness.discoverySha256 === sha2(JSON.stringify(binding)), "RECONCILE_DISCOVERY_RECOVERY_CHANGED");
|
|
18321
18337
|
unchanged2(proof.before);
|
|
18322
18338
|
unchanged2(proof.current);
|
|
18339
|
+
proof.recheckAdditions();
|
|
18323
18340
|
}
|
|
18324
18341
|
function verifiesCodexHookDiscoverySource(binding, source, currentDigest, witness) {
|
|
18325
18342
|
if (!witness || source.path !== witness.configPath || source.hashMode !== "bytes")
|
|
@@ -18361,13 +18378,13 @@ function read2(path, changes) {
|
|
|
18361
18378
|
return readFileSync16(path, "utf8");
|
|
18362
18379
|
}
|
|
18363
18380
|
function projected(source, changes, budget = discoveryByteBudget()) {
|
|
18364
|
-
if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2") {
|
|
18381
|
+
if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3") {
|
|
18365
18382
|
if (source.format !== undefined || source.fields !== undefined || source.managedPlugins !== undefined || typeof source.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(source.sha256))
|
|
18366
18383
|
throw new Error("Claude settings witnesses require exact typed metadata");
|
|
18367
|
-
const current2 = (source.hashMode === "claude-settings-v2" ? captureClaudeSettingsV2 : captureClaudeSettings)(source.path, budget).sha256;
|
|
18384
|
+
const current2 = (source.hashMode === "claude-settings-v3" ? captureClaudeSettingsV3 : source.hashMode === "claude-settings-v2" ? captureClaudeSettingsV2 : captureClaudeSettings)(source.path, budget).sha256;
|
|
18368
18385
|
if (current2 !== source.sha256)
|
|
18369
18386
|
throw new Error(`Native discovery input changed; run skills hook install with a fresh discovery review: ${source.path}`);
|
|
18370
|
-
return changes?.has(source.path) ? (source.hashMode === "claude-settings-v2" ? hashClaudeSettingsReplacementV2 : hashClaudeSettingsReplacement)(changes.get(source.path), budget) : current2;
|
|
18387
|
+
return changes?.has(source.path) ? (source.hashMode === "claude-settings-v3" ? hashClaudeSettingsReplacementV3 : source.hashMode === "claude-settings-v2" ? hashClaudeSettingsReplacementV2 : hashClaudeSettingsReplacement)(changes.get(source.path), budget) : current2;
|
|
18371
18388
|
}
|
|
18372
18389
|
if (source.hashMode === "claude-marketplace-registry" || source.hashMode === "claude-marketplace-registry-v2") {
|
|
18373
18390
|
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))
|
|
@@ -18429,7 +18446,7 @@ function verifyAgentDiscovery(binding, codexRecovery) {
|
|
|
18429
18446
|
safe4(root2);
|
|
18430
18447
|
}
|
|
18431
18448
|
function assertMarketplaceBinding(binding, source) {
|
|
18432
|
-
if ((source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2") && (binding.agent !== "claude" || binding.method !== "reviewed" || basename8(source.path) !== "settings.json"))
|
|
18449
|
+
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"))
|
|
18433
18450
|
throw new Error("Claude settings witnesses require explicit reviewed Claude configuration");
|
|
18434
18451
|
if ((source.hashMode === "claude-marketplace-registry" || source.hashMode === "claude-marketplace-registry-v2") && (binding.agent !== "claude" || binding.method !== "reviewed"))
|
|
18435
18452
|
throw new Error("Claude marketplace witnesses require explicit reviewed Claude discovery");
|
|
@@ -18438,7 +18455,10 @@ function rebindAgentDiscovery(binding, changes) {
|
|
|
18438
18455
|
const budget = discoveryByteBudget();
|
|
18439
18456
|
return { ...binding, sources: binding.sources.map((source) => {
|
|
18440
18457
|
assertMarketplaceBinding(binding, source);
|
|
18441
|
-
|
|
18458
|
+
const sha2562 = projected(source, changes, budget);
|
|
18459
|
+
if (!changes.has(source.path) && sha2562 !== source.sha256)
|
|
18460
|
+
throw new Error(`Native discovery input changed; run skills hook install with a fresh discovery review: ${source.path}`);
|
|
18461
|
+
return { ...source, sha256: sha2562 };
|
|
18442
18462
|
}) };
|
|
18443
18463
|
}
|
|
18444
18464
|
function assertProjectDiscovery(agent, directories, home, canonical4 = resolve14, reviewed) {
|
|
@@ -18550,6 +18570,19 @@ function resolveAgentDiscovery(options) {
|
|
|
18550
18570
|
}
|
|
18551
18571
|
}
|
|
18552
18572
|
}
|
|
18573
|
+
if (options.reviewed === undefined && options.retainedReview !== undefined) {
|
|
18574
|
+
const retained = options.retainedReview;
|
|
18575
|
+
if (retained.agent !== agent || retained.method !== "reviewed")
|
|
18576
|
+
unresolved("invalid retained review");
|
|
18577
|
+
verifyAgentDiscovery(retained);
|
|
18578
|
+
if (!retained.sources.some((source) => source.path === canonical4(configPath) && source.format === undefined && source.fields === undefined))
|
|
18579
|
+
unresolved("retained review is missing the full agent configuration source");
|
|
18580
|
+
if (retained.sources.some((source) => (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3") && source.path !== canonical4(configPath)))
|
|
18581
|
+
unresolved("retained settings witness names another configuration source");
|
|
18582
|
+
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))
|
|
18583
|
+
unresolved("retained review is missing current runtime discovery coverage");
|
|
18584
|
+
return retained;
|
|
18585
|
+
}
|
|
18553
18586
|
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))
|
|
18554
18587
|
throw new Error("Invalid --discovery-inputs version or agents");
|
|
18555
18588
|
const review = options.reviewed?.agents.find((item) => item.agent === agent);
|
|
@@ -18560,7 +18593,7 @@ function resolveAgentDiscovery(options) {
|
|
|
18560
18593
|
if (review.sources.some((source) => source.format !== undefined || source.fields !== undefined))
|
|
18561
18594
|
throw new Error("Explicit discovery reviews require full source-file hashes");
|
|
18562
18595
|
verifyAgentDiscovery(supplied);
|
|
18563
|
-
if (review.sources.some((source) => (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2") && source.path !== canonical4(configPath)))
|
|
18596
|
+
if (review.sources.some((source) => (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3") && source.path !== canonical4(configPath)))
|
|
18564
18597
|
throw new Error("Semantic settings witnesses must name the configured Claude configuration source");
|
|
18565
18598
|
if (!review.sources.some((source) => source.path === canonical4(configPath)))
|
|
18566
18599
|
throw new Error("Discovery review must include the agent configuration source");
|
|
@@ -39070,8 +39103,8 @@ var SESSION_RECONCILIATION_PLAN_TTL_MS = 5 * 60 * 1000;
|
|
|
39070
39103
|
// src/lib/claude-skills-coordination.ts
|
|
39071
39104
|
function coordinateClaudeSettings(before, replacement, options) {
|
|
39072
39105
|
const prior = JSON.parse(before ?? "{}"), next = JSON.parse(replacement);
|
|
39073
|
-
const
|
|
39074
|
-
const changed = [...
|
|
39106
|
+
const events2 = new Set([...Object.keys(prior.hooks ?? {}), ...Object.keys(next.hooks ?? {})]);
|
|
39107
|
+
const changed = [...events2].filter((event) => JSON.stringify(prior.hooks?.[event]) !== JSON.stringify(next.hooks?.[event]));
|
|
39075
39108
|
const selected = changed.length ? changed : ["PreToolUse"];
|
|
39076
39109
|
const plan = planClaudeHookEventsUpdate({
|
|
39077
39110
|
...options,
|
|
@@ -39359,7 +39392,7 @@ function detectConflict(name, scope, target) {
|
|
|
39359
39392
|
const meta3 = isNativeSafetyName(name) ? getHook(name) : resolveHookMeta(name);
|
|
39360
39393
|
if (!meta3 || !meta3.matcher)
|
|
39361
39394
|
return;
|
|
39362
|
-
const
|
|
39395
|
+
const events2 = new Set(getHookEvents(meta3));
|
|
39363
39396
|
const registered = getRegisteredHooksForTarget(scope, target);
|
|
39364
39397
|
for (const existingName of registered) {
|
|
39365
39398
|
if (existingName === name)
|
|
@@ -39367,7 +39400,7 @@ function detectConflict(name, scope, target) {
|
|
|
39367
39400
|
const existing = getHook(existingName);
|
|
39368
39401
|
if (!existing || !existing.matcher)
|
|
39369
39402
|
continue;
|
|
39370
|
-
if (!getHookEvents(existing).some((event) =>
|
|
39403
|
+
if (!getHookEvents(existing).some((event) => events2.has(event)))
|
|
39371
39404
|
continue;
|
|
39372
39405
|
if (matchersOverlap(meta3.matcher.toLowerCase(), existing.matcher.toLowerCase())) {
|
|
39373
39406
|
return `conflicts with '${existingName}' (same event ${meta3.event}, overlapping matcher '${existing.matcher}')`;
|
|
@@ -12,5 +12,9 @@ export declare function isNativeSafetyName(name: string): name is NativeSafetyNa
|
|
|
12
12
|
export declare function buildNativeSafetyCommand(binding: NativeSafetyBinding): string;
|
|
13
13
|
export declare function nativeSafetyRegistration(command: string): NativeSafetyName | undefined;
|
|
14
14
|
export declare function nativeSafetyCommandBinding(command: string): NativeSafetyBinding | undefined;
|
|
15
|
+
/** Whether a saved supervisor command's pinned runtime and worker still verify
|
|
16
|
+
* on disk. `undefined` means the command is not a native-safety binding at all.
|
|
17
|
+
* Read-only: it never registers, rewrites or executes anything. */
|
|
18
|
+
export declare function nativeSafetyBindingVerified(command: string): boolean | undefined;
|
|
15
19
|
/** Built-in capability only: no registry, custom source, profile or app store. */
|
|
16
20
|
export declare function installedNativeSafetyCommand(name: NativeSafetyName): string;
|
package/dist/native-safety.d.ts
CHANGED
|
@@ -43,6 +43,12 @@ export declare function readNativeSafetyRegistration(home?: string): {
|
|
|
43
43
|
sha256: string;
|
|
44
44
|
path: string;
|
|
45
45
|
};
|
|
46
|
+
/** Bounded, non-leaking explanation for a refused binding: which integrity
|
|
47
|
+
* property the saved record fails. A binding that a third party could rewrite
|
|
48
|
+
* can point the supervisor at a different worker, so its owner and mode are part
|
|
49
|
+
* of the trust decision, not decoration. Read-only — it never repairs, rewrites
|
|
50
|
+
* or executes anything, and returns undefined when the record looks intact. */
|
|
51
|
+
export declare function nativeSafetyBindingIntegrityReason(home?: string): string | undefined;
|
|
46
52
|
/** Register only the bundled composite guard. Updates preserve the exact prior
|
|
47
53
|
* record and require its digest. A lock coordinates package-owned writers; an
|
|
48
54
|
* unexpected preimage or a retained interrupted lock requires reconciliation. */
|
package/dist/native-safety.js
CHANGED
|
@@ -210,6 +210,23 @@ function readNativeSafetyRegistration(home = homedir2()) {
|
|
|
210
210
|
throw new NativeSafetyError(error?.code === "ENOENT" ? "binding_missing" : "binding_invalid");
|
|
211
211
|
}
|
|
212
212
|
}
|
|
213
|
+
function nativeSafetyBindingIntegrityReason(home = homedir2()) {
|
|
214
|
+
try {
|
|
215
|
+
const path = join2(canonicalHome(home), ".hasna/hooks/native", filename);
|
|
216
|
+
const stat = lstatSync2(path);
|
|
217
|
+
if (!stat.isFile())
|
|
218
|
+
return "the saved binding is not a regular file";
|
|
219
|
+
if (!owner(stat.uid))
|
|
220
|
+
return "the saved binding is not owned by the current user";
|
|
221
|
+
if (stat.mode & 63)
|
|
222
|
+
return `the saved binding is accessible by group or others (mode ${(stat.mode & 511).toString(8).padStart(3, "0")})`;
|
|
223
|
+
if (stat.size > 32768)
|
|
224
|
+
return "the saved binding exceeds its size bound";
|
|
225
|
+
return;
|
|
226
|
+
} catch (error) {
|
|
227
|
+
return error?.code === "ENOENT" ? "the saved binding is missing" : undefined;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
213
230
|
function registerNativeSafety(options = {}) {
|
|
214
231
|
const home = canonicalHome(options.home ?? homedir2());
|
|
215
232
|
const record = { schema: "hasna.hooks.native-safety.v1", harness: "sumi", command: options.command ?? installedNativeSafetyCommand("trash-guard") };
|
|
@@ -395,6 +412,7 @@ export {
|
|
|
395
412
|
verifyNativeSafetyCommand,
|
|
396
413
|
registerNativeSafety,
|
|
397
414
|
readNativeSafetyRegistration,
|
|
415
|
+
nativeSafetyBindingIntegrityReason,
|
|
398
416
|
evaluateNativeSafetyForExecution,
|
|
399
417
|
evaluateNativeSafety,
|
|
400
418
|
NativeSafetyError
|
|
@@ -45,6 +45,18 @@ a newline or `||` the `cd` may have failed, so the earlier directories stay
|
|
|
45
45
|
in scope. A `cd` inside `( ... )`, a pipeline or the background does not move
|
|
46
46
|
it, and neither does a `cd` whose target cannot be resolved statically.
|
|
47
47
|
Descriptor duplication (`2>&1`, `>&2`, `1>&-`) is not a write.
|
|
48
|
+
Each pipeline stage is classified on its own, with its output redirections
|
|
49
|
+
split off: `ls <root> 2>/dev/null | head` reads `<root>`, and the trailing
|
|
50
|
+
word of a later read-only stage (`| head -60`) is never taken as the write
|
|
51
|
+
target of an earlier stage. Pipes inside quotes or command substitutions are
|
|
52
|
+
not stage boundaries.
|
|
53
|
+
A protected path named anywhere in a pipeline takes the strongest operation
|
|
54
|
+
of any stage (`find <root>/x | xargs rm -rf` is a delete). Deletes and
|
|
55
|
+
`mkdir`/`touch` check every operand; other writers check their last one.
|
|
56
|
+
An operand or redirect target that cannot be read statically (a variable
|
|
57
|
+
other than `$HOME`, `$( )`, backticks, a glob) fails closed to the directory
|
|
58
|
+
the command runs in, so `rm -rf "$TMPDIR/x"` run from a protected checkout is
|
|
59
|
+
refused. Use a literal absolute path for deletes outside the roots.
|
|
48
60
|
|
|
49
61
|
## Configuration
|
|
50
62
|
|