@hasna/hooks 0.9.5 → 0.9.6
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 +27 -0
- package/bin/hooks-mcp.js +282 -123
- package/bin/index.js +625 -252
- package/bin/native-safety-entry.js +1136 -0
- package/bin/serve.js +329 -127
- package/dist/index.js +610 -249
- package/dist/lib/native-safety.d.ts +15 -0
- package/dist/openapi.d.ts +314 -45
- package/dist/sdk/authority.d.ts +31 -0
- package/dist/sdk/generated.d.ts +97 -0
- package/dist/sdk/index.d.ts +14 -15
- package/dist/sdk/index.js +172 -1
- package/dist/sdk/registry-client.d.ts +75 -0
- package/hooks/native-safety-entry.ts +28 -0
- package/package.json +7 -4
- package/scripts/generate-sdk.ts +60 -0
- package/scripts/validate-package.ts +34 -0
- package/scripts/verify-generated-artifacts.ts +237 -0
package/bin/hooks-mcp.js
CHANGED
|
@@ -206,6 +206,7 @@ var init_registry = __esm(() => {
|
|
|
206
206
|
},
|
|
207
207
|
{
|
|
208
208
|
name: "workspace-repos-guard",
|
|
209
|
+
timeoutSeconds: 10,
|
|
209
210
|
displayName: "Workspace Repos Guard",
|
|
210
211
|
description: "Blocks writes and deletes that break the protected repo-checkout structure (~/.hasna/repos/clones canonical, plus legacy ~/workspace/repos during the migration window): writes to a protected root, top-level entry creation, non-org folders, and any delete under a protected root",
|
|
211
212
|
version: "0.1.0",
|
|
@@ -224,7 +225,7 @@ var init_registry = __esm(() => {
|
|
|
224
225
|
matcher: "^(Bash|apply_patch|ApplyPatch|functions\\.apply_patch)$",
|
|
225
226
|
tags: ["rm", "delete", "trash", "recoverable", "guard", "safety", "bash"],
|
|
226
227
|
rewritesInput: true,
|
|
227
|
-
timeoutSeconds:
|
|
228
|
+
timeoutSeconds: 10
|
|
228
229
|
},
|
|
229
230
|
{
|
|
230
231
|
name: "checktests",
|
|
@@ -1129,9 +1130,9 @@ var init_migrations = __esm(() => {
|
|
|
1129
1130
|
});
|
|
1130
1131
|
|
|
1131
1132
|
// src/db/legacy-import.ts
|
|
1132
|
-
import { existsSync as
|
|
1133
|
-
import { join as
|
|
1134
|
-
import { homedir as
|
|
1133
|
+
import { existsSync as existsSync5, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "fs";
|
|
1134
|
+
import { join as join6 } from "path";
|
|
1135
|
+
import { homedir as homedir3 } from "os";
|
|
1135
1136
|
function ensureMetaTable(db) {
|
|
1136
1137
|
db.exec(`
|
|
1137
1138
|
CREATE TABLE IF NOT EXISTS _meta (
|
|
@@ -1154,7 +1155,7 @@ function nanoid() {
|
|
|
1154
1155
|
function importJsonlFile(db, filePath) {
|
|
1155
1156
|
let count = 0;
|
|
1156
1157
|
try {
|
|
1157
|
-
const lines =
|
|
1158
|
+
const lines = readFileSync3(filePath, "utf-8").split(`
|
|
1158
1159
|
`).filter(Boolean);
|
|
1159
1160
|
for (const line of lines) {
|
|
1160
1161
|
try {
|
|
@@ -1180,7 +1181,7 @@ function importJsonlFile(db, filePath) {
|
|
|
1180
1181
|
function importErrorsLog(db, filePath) {
|
|
1181
1182
|
let count = 0;
|
|
1182
1183
|
try {
|
|
1183
|
-
const lines =
|
|
1184
|
+
const lines = readFileSync3(filePath, "utf-8").split(`
|
|
1184
1185
|
`).filter(Boolean);
|
|
1185
1186
|
const linePattern = /^\[(.+?)\]\s+(?:\[session:(\S+)\]\s+)?(.+?)\s+\u2014\s+(.+)$/;
|
|
1186
1187
|
for (const line of lines) {
|
|
@@ -1205,25 +1206,25 @@ function importErrorsLog(db, filePath) {
|
|
|
1205
1206
|
} catch {}
|
|
1206
1207
|
return count;
|
|
1207
1208
|
}
|
|
1208
|
-
function runLegacyImport(db, homeDir =
|
|
1209
|
+
function runLegacyImport(db, homeDir = homedir3()) {
|
|
1209
1210
|
try {
|
|
1210
1211
|
if (isAlreadyDone(db))
|
|
1211
1212
|
return;
|
|
1212
1213
|
let total = 0;
|
|
1213
|
-
const claudeProjectsDir =
|
|
1214
|
-
if (
|
|
1214
|
+
const claudeProjectsDir = join6(homeDir, ".claude", "projects");
|
|
1215
|
+
if (existsSync5(claudeProjectsDir)) {
|
|
1215
1216
|
try {
|
|
1216
1217
|
const projectDirs = readdirSync2(claudeProjectsDir);
|
|
1217
1218
|
for (const dir of projectDirs) {
|
|
1218
|
-
const projectDir =
|
|
1219
|
+
const projectDir = join6(claudeProjectsDir, dir);
|
|
1219
1220
|
try {
|
|
1220
1221
|
const files = readdirSync2(projectDir);
|
|
1221
1222
|
for (const file of files) {
|
|
1222
1223
|
if (file.match(/^session-log-\d{4}-\d{2}-\d{2}\.jsonl$/)) {
|
|
1223
|
-
total += importJsonlFile(db,
|
|
1224
|
+
total += importJsonlFile(db, join6(projectDir, file));
|
|
1224
1225
|
}
|
|
1225
1226
|
if (file === "errors.log") {
|
|
1226
|
-
total += importErrorsLog(db,
|
|
1227
|
+
total += importErrorsLog(db, join6(projectDir, file));
|
|
1227
1228
|
}
|
|
1228
1229
|
}
|
|
1229
1230
|
} catch {}
|
|
@@ -1270,8 +1271,8 @@ __export(exports_db, {
|
|
|
1270
1271
|
allowLocalStore: () => allowLocalStore
|
|
1271
1272
|
});
|
|
1272
1273
|
import { Database } from "bun:sqlite";
|
|
1273
|
-
import { existsSync as
|
|
1274
|
-
import { join as
|
|
1274
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync2, cpSync } from "fs";
|
|
1275
|
+
import { join as join7 } from "path";
|
|
1275
1276
|
function refuseLocalStore(message) {
|
|
1276
1277
|
localStoreRefusal = message;
|
|
1277
1278
|
}
|
|
@@ -1286,8 +1287,8 @@ function localStoreRefusalMessage() {
|
|
|
1286
1287
|
}
|
|
1287
1288
|
function resolveDataDir() {
|
|
1288
1289
|
const effective = getEffectiveDataRoot();
|
|
1289
|
-
const oldDir =
|
|
1290
|
-
if (!
|
|
1290
|
+
const oldDir = join7(getHomeDir(), ".hooks");
|
|
1291
|
+
if (!existsSync6(effective) && existsSync6(oldDir)) {
|
|
1291
1292
|
mkdirSync2(effective, { recursive: true });
|
|
1292
1293
|
cpSync(oldDir, effective, { recursive: true });
|
|
1293
1294
|
}
|
|
@@ -1298,11 +1299,11 @@ function getDbPath() {
|
|
|
1298
1299
|
if (explicitDb)
|
|
1299
1300
|
return explicitDb;
|
|
1300
1301
|
const dataDir2 = resolveDataDir();
|
|
1301
|
-
return
|
|
1302
|
+
return join7(dataDir2, "hooks.db");
|
|
1302
1303
|
}
|
|
1303
1304
|
function ensureDir(dbPath) {
|
|
1304
1305
|
const dir = dbPath.substring(0, dbPath.lastIndexOf("/"));
|
|
1305
|
-
if (dir && !
|
|
1306
|
+
if (dir && !existsSync6(dir)) {
|
|
1306
1307
|
mkdirSync2(dir, { recursive: true });
|
|
1307
1308
|
}
|
|
1308
1309
|
}
|
|
@@ -1312,7 +1313,7 @@ function getDb() {
|
|
|
1312
1313
|
if (instance)
|
|
1313
1314
|
return instance;
|
|
1314
1315
|
const dbPath = getDbPath();
|
|
1315
|
-
const isNew = dbPath === ":memory:" || !
|
|
1316
|
+
const isNew = dbPath === ":memory:" || !existsSync6(dbPath);
|
|
1316
1317
|
ensureDir(dbPath);
|
|
1317
1318
|
instance = new Database(dbPath);
|
|
1318
1319
|
instance.exec("PRAGMA busy_timeout=5000");
|
|
@@ -1421,10 +1422,10 @@ var init_redact = __esm(() => {
|
|
|
1421
1422
|
// ../contracts/dist/client/transport.js
|
|
1422
1423
|
import { isIP } from "net";
|
|
1423
1424
|
import { spawnSync } from "child_process";
|
|
1424
|
-
import { closeSync as
|
|
1425
|
+
import { closeSync as closeSync22, fstatSync as fstatSync2, openSync as openSync22, readFileSync as readFileSync22 } from "fs";
|
|
1425
1426
|
import { O_NOFOLLOW as O_NOFOLLOW2, O_NONBLOCK as O_NONBLOCK2, O_RDONLY as O_RDONLY2 } from "constants";
|
|
1426
1427
|
import { hostname as osHostname } from "os";
|
|
1427
|
-
import { isAbsolute as
|
|
1428
|
+
import { isAbsolute as isAbsolute3, join as join22 } from "path";
|
|
1428
1429
|
import { createRequire } from "module";
|
|
1429
1430
|
function envToken(name) {
|
|
1430
1431
|
return name.toUpperCase().replace(/-/g, "_");
|
|
@@ -1448,7 +1449,7 @@ function homeDir(env) {
|
|
|
1448
1449
|
}
|
|
1449
1450
|
function absoluteOverride(env, key) {
|
|
1450
1451
|
const value = env[key]?.trim();
|
|
1451
|
-
return value &&
|
|
1452
|
+
return value && isAbsolute3(value) ? value : null;
|
|
1452
1453
|
}
|
|
1453
1454
|
function hasnaHomeDir(env) {
|
|
1454
1455
|
const override = absoluteOverride(env, HASNA_HOME_ENV_KEY);
|
|
@@ -1494,9 +1495,9 @@ function parseEnvFile(text) {
|
|
|
1494
1495
|
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(key))
|
|
1495
1496
|
continue;
|
|
1496
1497
|
let value = withoutExport.slice(equals + 1).trim();
|
|
1497
|
-
const
|
|
1498
|
-
if (
|
|
1499
|
-
if (value.length < 2 || !value.endsWith(
|
|
1498
|
+
const quote2 = value[0];
|
|
1499
|
+
if (quote2 === '"' || quote2 === "'") {
|
|
1500
|
+
if (value.length < 2 || !value.endsWith(quote2)) {
|
|
1500
1501
|
unusable.add(key);
|
|
1501
1502
|
continue;
|
|
1502
1503
|
}
|
|
@@ -1525,7 +1526,7 @@ function readAppConfigFile(path) {
|
|
|
1525
1526
|
};
|
|
1526
1527
|
let fd = -1;
|
|
1527
1528
|
try {
|
|
1528
|
-
fd =
|
|
1529
|
+
fd = openSync22(path, O_RDONLY2 | O_NOFOLLOW2 | O_NONBLOCK2);
|
|
1529
1530
|
} catch (error) {
|
|
1530
1531
|
const code = error.code;
|
|
1531
1532
|
if (code === "ENOENT" || code === "ENOTDIR")
|
|
@@ -1554,7 +1555,7 @@ function readAppConfigFile(path) {
|
|
|
1554
1555
|
return parseEnvFile(bytes.toString("utf8"));
|
|
1555
1556
|
} finally {
|
|
1556
1557
|
if (fd !== -1)
|
|
1557
|
-
|
|
1558
|
+
closeSync22(fd);
|
|
1558
1559
|
}
|
|
1559
1560
|
}
|
|
1560
1561
|
function readCredentialFile(path, apiKeyKeys, pointerKey) {
|
|
@@ -2228,12 +2229,12 @@ var init_local_opt_in = __esm(() => {
|
|
|
2228
2229
|
});
|
|
2229
2230
|
|
|
2230
2231
|
// src/lib/hook-env.ts
|
|
2231
|
-
import { dirname as
|
|
2232
|
-
import { existsSync as
|
|
2232
|
+
import { dirname as dirname6, isAbsolute as isAbsolute4 } from "path";
|
|
2233
|
+
import { existsSync as existsSync11, realpathSync as realpathSync3, statSync } from "fs";
|
|
2233
2234
|
import { tmpdir } from "os";
|
|
2234
2235
|
function isDeniedEnvName(name) {
|
|
2235
2236
|
const upper = name.toUpperCase();
|
|
2236
|
-
if (DENY_PREFIXES.some((
|
|
2237
|
+
if (DENY_PREFIXES.some((prefix2) => upper.startsWith(prefix2)))
|
|
2237
2238
|
return true;
|
|
2238
2239
|
if (DENY_SUFFIXES.some((suffix) => upper.endsWith(suffix)))
|
|
2239
2240
|
return true;
|
|
@@ -2242,13 +2243,13 @@ function isDeniedEnvName(name) {
|
|
|
2242
2243
|
function isInterpreterInjectionEnvName(name) {
|
|
2243
2244
|
if (INTERPRETER_INJECTION.has(name))
|
|
2244
2245
|
return true;
|
|
2245
|
-
return INTERPRETER_INJECTION_PREFIXES.some((
|
|
2246
|
+
return INTERPRETER_INJECTION_PREFIXES.some((prefix2) => name.startsWith(prefix2));
|
|
2246
2247
|
}
|
|
2247
2248
|
function isBashFunctionEnvName(name) {
|
|
2248
2249
|
return name.startsWith("BASH_FUNC_");
|
|
2249
2250
|
}
|
|
2250
2251
|
function isUnsafePathEntry(entry, home) {
|
|
2251
|
-
if (entry === "" || !
|
|
2252
|
+
if (entry === "" || !isAbsolute4(entry))
|
|
2252
2253
|
return true;
|
|
2253
2254
|
if (home && (entry === home || entry.startsWith(home + "/")))
|
|
2254
2255
|
return true;
|
|
@@ -2258,7 +2259,7 @@ function isUnsafePathEntry(entry, home) {
|
|
|
2258
2259
|
return true;
|
|
2259
2260
|
const canonical = (path) => {
|
|
2260
2261
|
try {
|
|
2261
|
-
return
|
|
2262
|
+
return realpathSync3(path);
|
|
2262
2263
|
} catch {
|
|
2263
2264
|
return path;
|
|
2264
2265
|
}
|
|
@@ -2277,9 +2278,9 @@ function isUnsafePathEntry(entry, home) {
|
|
|
2277
2278
|
}
|
|
2278
2279
|
function sanitizeHookPath(pathValue, home) {
|
|
2279
2280
|
const baseline = PATH_BASELINE.slice();
|
|
2280
|
-
if (process.platform === "darwin" &&
|
|
2281
|
+
if (process.platform === "darwin" && existsSync11("/opt/homebrew/bin"))
|
|
2281
2282
|
baseline.push("/opt/homebrew/bin");
|
|
2282
|
-
const bunDir =
|
|
2283
|
+
const bunDir = dirname6(process.execPath);
|
|
2283
2284
|
const seen = new Set;
|
|
2284
2285
|
const out = [];
|
|
2285
2286
|
for (const dir of baseline) {
|
|
@@ -2404,7 +2405,7 @@ __export(exports_run, {
|
|
|
2404
2405
|
});
|
|
2405
2406
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
2406
2407
|
import { rmSync as rmSync2, writeFileSync as writeFileSync6 } from "fs";
|
|
2407
|
-
import { basename as basename2, dirname as
|
|
2408
|
+
import { basename as basename2, dirname as dirname7, isAbsolute as isAbsolute5, join as join10 } from "path";
|
|
2408
2409
|
function extensionOf(scriptPath) {
|
|
2409
2410
|
const base = basename2(scriptPath);
|
|
2410
2411
|
const dot = base.lastIndexOf(".");
|
|
@@ -2473,9 +2474,9 @@ async function readPipeWithDeadline(stream, deadlineMs) {
|
|
|
2473
2474
|
return chunks.join("");
|
|
2474
2475
|
}
|
|
2475
2476
|
async function executeVerifiedScript(options) {
|
|
2476
|
-
const scriptDir =
|
|
2477
|
+
const scriptDir = dirname7(options.scriptPath);
|
|
2477
2478
|
const interpreter = interpreterFor(options.name, options.scriptPath, options.content);
|
|
2478
|
-
const tempPath =
|
|
2479
|
+
const tempPath = join10(scriptDir, `.hooks-verified-${randomBytes2(12).toString("hex")}.${interpreter.tempExt}`);
|
|
2479
2480
|
let proc = null;
|
|
2480
2481
|
let timedOut = false;
|
|
2481
2482
|
const timeoutTimer = options.timeout ? setTimeout(() => {
|
|
@@ -2484,7 +2485,7 @@ async function executeVerifiedScript(options) {
|
|
|
2484
2485
|
}, options.timeout) : null;
|
|
2485
2486
|
try {
|
|
2486
2487
|
writeFileSync6(tempPath, options.content, { flag: "wx", mode: 384 });
|
|
2487
|
-
proc = Bun.spawn([...interpreter.command,
|
|
2488
|
+
proc = Bun.spawn([...interpreter.command, isAbsolute5(tempPath) ? tempPath : `./${tempPath}`, ...options.args ?? []], {
|
|
2488
2489
|
stdin: new Response(options.stdin),
|
|
2489
2490
|
stdout: "pipe",
|
|
2490
2491
|
stderr: "pipe",
|
|
@@ -2683,19 +2684,150 @@ import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
|
|
|
2683
2684
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
2684
2685
|
import { z as z2 } from "zod";
|
|
2685
2686
|
init_registry();
|
|
2686
|
-
import { existsSync as
|
|
2687
|
-
import { join as
|
|
2687
|
+
import { existsSync as existsSync12, readFileSync as readFileSync8 } from "fs";
|
|
2688
|
+
import { join as join11, dirname as dirname8 } from "path";
|
|
2688
2689
|
import { fileURLToPath as fileURLToPath3 } from "url";
|
|
2689
2690
|
|
|
2690
2691
|
// src/lib/installer.ts
|
|
2691
2692
|
init_registry();
|
|
2692
2693
|
init_resolve();
|
|
2693
2694
|
init_manifest();
|
|
2694
|
-
import { existsSync as
|
|
2695
|
-
import { join as
|
|
2696
|
-
import { homedir as
|
|
2695
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync5, readFileSync as readFileSync6, readdirSync as readdirSync3, rmdirSync, unlinkSync as unlinkSync2, writeFileSync as writeFileSync4 } from "fs";
|
|
2696
|
+
import { join as join8, dirname as dirname5 } from "path";
|
|
2697
|
+
import { homedir as homedir4 } from "os";
|
|
2697
2698
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2698
2699
|
|
|
2700
|
+
// src/lib/native-safety.ts
|
|
2701
|
+
import { createHash } from "crypto";
|
|
2702
|
+
import { closeSync, constants, existsSync as existsSync4, fstatSync, lstatSync, openSync, readFileSync as readFileSync2, realpathSync as realpathSync2 } from "fs";
|
|
2703
|
+
import { dirname as dirname3, isAbsolute as isAbsolute2, join as join5 } from "path";
|
|
2704
|
+
import { homedir as homedir2 } from "os";
|
|
2705
|
+
function isNativeSafetyName(name) {
|
|
2706
|
+
return name === "trash-guard" || name === "workspace-repos-guard";
|
|
2707
|
+
}
|
|
2708
|
+
var supervisorSource = String.raw`async function supervise(encoded) {
|
|
2709
|
+
const fs = require("node:fs");
|
|
2710
|
+
const path = require("node:path");
|
|
2711
|
+
const crypto = require("node:crypto");
|
|
2712
|
+
const cp = require("node:child_process");
|
|
2713
|
+
const fail = () => { process.stderr.write("Native safety guard refused: verification or execution failed.\n"); process.exit(2); };
|
|
2714
|
+
try {
|
|
2715
|
+
const b = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
|
|
2716
|
+
const check = (file, hash, maximum) => {
|
|
2717
|
+
if (!path.isAbsolute(file) || fs.realpathSync(file) !== file) throw Error();
|
|
2718
|
+
for (let parent = path.dirname(file);;) {
|
|
2719
|
+
const s = fs.lstatSync(parent);
|
|
2720
|
+
if (!s.isDirectory() || ![0, process.getuid()].includes(s.uid) || (s.mode & 0o022)) throw Error();
|
|
2721
|
+
if (parent === path.dirname(parent)) break;
|
|
2722
|
+
parent = path.dirname(parent);
|
|
2723
|
+
}
|
|
2724
|
+
const fd = fs.openSync(file, fs.constants.O_RDONLY | fs.constants.O_NOFOLLOW);
|
|
2725
|
+
try {
|
|
2726
|
+
const s = fs.fstatSync(fd);
|
|
2727
|
+
if (!s.isFile() || ![0, process.getuid()].includes(s.uid) || (s.mode & 0o022) || s.size > maximum) throw Error();
|
|
2728
|
+
const bytes = fs.readFileSync(fd);
|
|
2729
|
+
if (crypto.createHash("sha256").update(bytes).digest("hex") !== hash) throw Error();
|
|
2730
|
+
return bytes;
|
|
2731
|
+
} finally { fs.closeSync(fd); }
|
|
2732
|
+
};
|
|
2733
|
+
check(b.runtime, b.runtimeSHA256, 200_000_000);
|
|
2734
|
+
const bytes = check(b.worker, b.workerSHA256, 256_000);
|
|
2735
|
+
const deadline = setTimeout(fail, 1500);
|
|
2736
|
+
const chunks = []; let size = 0;
|
|
2737
|
+
for await (const chunk of process.stdin) {
|
|
2738
|
+
size += chunk.length;
|
|
2739
|
+
if (size > 1_048_576) throw Error();
|
|
2740
|
+
chunks.push(chunk);
|
|
2741
|
+
}
|
|
2742
|
+
clearTimeout(deadline);
|
|
2743
|
+
const input = Buffer.concat(chunks);
|
|
2744
|
+
const child = cp.spawnSync(b.runtime, ["--config=/dev/null", "--no-env-file", "--no-macros", "--no-install", "--eval", bytes.toString("utf8"), b.name], {
|
|
2745
|
+
input, env: { HOME: b.home, PATH: b.path }, cwd: "/", timeout: 2500, killSignal: "SIGKILL", maxBuffer: 1_048_576,
|
|
2746
|
+
});
|
|
2747
|
+
if (child.error || child.status !== 0 || child.signal || child.stderr.length) throw Error();
|
|
2748
|
+
const result = JSON.parse(child.stdout.toString("utf8"));
|
|
2749
|
+
if (!result || Array.isArray(result) || Object.keys(result).length !== 1) throw Error();
|
|
2750
|
+
if (result.verdict === null) return;
|
|
2751
|
+
const output = result.verdict?.hookSpecificOutput;
|
|
2752
|
+
if (!output || Object.keys(result.verdict).length !== 1 || output.hookEventName !== "PreToolUse") throw Error();
|
|
2753
|
+
if (output.permissionDecision === "deny") {
|
|
2754
|
+
if (Object.keys(output).sort().join() !== "hookEventName,permissionDecision,permissionDecisionReason" || typeof output.permissionDecisionReason !== "string" || !output.permissionDecisionReason.trim()) throw Error();
|
|
2755
|
+
} else if (output.permissionDecision === "allow") {
|
|
2756
|
+
if (b.name !== "trash-guard" || Object.keys(output).sort().join() !== "hookEventName,permissionDecision,updatedInput" || !output.updatedInput || typeof output.updatedInput.command !== "string" || !output.updatedInput.command.trim()) throw Error();
|
|
2757
|
+
} else throw Error();
|
|
2758
|
+
process.stdout.write(JSON.stringify(result.verdict));
|
|
2759
|
+
} catch { fail(); }
|
|
2760
|
+
}`;
|
|
2761
|
+
var quote = (value) => "'" + value.replaceAll("'", "'\\''") + "'";
|
|
2762
|
+
var program = `(${supervisorSource})(process.argv[1]);`;
|
|
2763
|
+
var shell = `cd / || exit 2
|
|
2764
|
+
exec 3<&0 || exit 2
|
|
2765
|
+
/usr/bin/env -i HOME="$3" PATH="$4" "$2" --config=/dev/null --no-env-file --no-macros --no-install --eval ${quote(program)} "$1" <&3 3<&- 2>/dev/null &
|
|
2766
|
+
runner=$!
|
|
2767
|
+
exec 3<&-
|
|
2768
|
+
(trap 'kill "$sleeper" 2>/dev/null; exit 0' TERM; /bin/sleep 6 & sleeper=$!; wait "$sleeper"; kill -KILL "$runner" 2>/dev/null) &
|
|
2769
|
+
watchdog=$!
|
|
2770
|
+
wait "$runner"; result=$?
|
|
2771
|
+
kill "$watchdog" 2>/dev/null; wait "$watchdog" 2>/dev/null
|
|
2772
|
+
if [ "$result" -ne 0 ]; then printf '%s\\n' 'Native safety guard refused: verification or execution failed.' >&2; exit 2; fi`;
|
|
2773
|
+
var prefix = `/usr/bin/env -i /bin/sh -c ${quote(shell)} hasna-native-safety-v1 `;
|
|
2774
|
+
function validateBinding(b) {
|
|
2775
|
+
if (!b || !isNativeSafetyName(b.name) || ![b.runtime, b.worker, b.home].every((p) => typeof p === "string" && isAbsolute2(p) && !/[\0\r\n]/.test(p)) || ![b.runtimeSHA256, b.workerSHA256].every((h) => /^[0-9a-f]{64}$/.test(h)) || typeof b.path !== "string" || b.path.split(":").some((p) => !isAbsolute2(p) || /[\0\r\n]/.test(p)))
|
|
2776
|
+
throw new Error("Invalid native safety binding");
|
|
2777
|
+
}
|
|
2778
|
+
function buildNativeSafetyCommand(binding) {
|
|
2779
|
+
validateBinding(binding);
|
|
2780
|
+
const encoded = Buffer.from(JSON.stringify(binding)).toString("base64url");
|
|
2781
|
+
return prefix + [encoded, binding.runtime, binding.home, binding.path].map(quote).join(" ");
|
|
2782
|
+
}
|
|
2783
|
+
function nativeSafetyRegistration(command) {
|
|
2784
|
+
if (!command.startsWith(prefix))
|
|
2785
|
+
return;
|
|
2786
|
+
const encoded = command.slice(prefix.length).match(/^'([A-Za-z0-9_-]+)' /)?.[1];
|
|
2787
|
+
if (!encoded || encoded.length > 16384)
|
|
2788
|
+
return;
|
|
2789
|
+
try {
|
|
2790
|
+
const binding = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
|
|
2791
|
+
return buildNativeSafetyCommand(binding) === command ? binding.name : undefined;
|
|
2792
|
+
} catch {
|
|
2793
|
+
return;
|
|
2794
|
+
}
|
|
2795
|
+
}
|
|
2796
|
+
function pinEligibleFile(file, maximum, executable = false) {
|
|
2797
|
+
if (!isAbsolute2(file) || realpathSync2(file) !== file)
|
|
2798
|
+
throw new Error("Native safety requires canonical files");
|
|
2799
|
+
for (let parent = dirname3(file);; ) {
|
|
2800
|
+
const s = lstatSync(parent);
|
|
2801
|
+
if (!s.isDirectory() || ![0, process.getuid()].includes(s.uid) || s.mode & 18)
|
|
2802
|
+
throw new Error("Native safety requires trusted, non-writable file ancestry");
|
|
2803
|
+
if (parent === dirname3(parent))
|
|
2804
|
+
break;
|
|
2805
|
+
parent = dirname3(parent);
|
|
2806
|
+
}
|
|
2807
|
+
const fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
2808
|
+
try {
|
|
2809
|
+
const s = fstatSync(fd);
|
|
2810
|
+
if (!s.isFile() || ![0, process.getuid()].includes(s.uid) || s.mode & 18 || s.size > maximum || executable && !(s.mode & 64))
|
|
2811
|
+
throw new Error("Native safety requires an eligible installed file");
|
|
2812
|
+
const bytes = readFileSync2(fd);
|
|
2813
|
+
if (bytes.length > maximum)
|
|
2814
|
+
throw new Error("Native safety file exceeds its bound");
|
|
2815
|
+
return createHash("sha256").update(bytes).digest("hex");
|
|
2816
|
+
} finally {
|
|
2817
|
+
closeSync(fd);
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
function installedNativeSafetyCommand(name) {
|
|
2821
|
+
const home = realpathSync2(homedir2());
|
|
2822
|
+
const runtime = realpathSync2(process.execPath);
|
|
2823
|
+
const candidates = [join5(import.meta.dir, "../../bin/native-safety-entry.js"), join5(import.meta.dir, "../bin/native-safety-entry.js"), join5(import.meta.dir, "native-safety-entry.js")];
|
|
2824
|
+
const worker = candidates.find(existsSync4);
|
|
2825
|
+
if (!worker)
|
|
2826
|
+
throw new Error("Bundled native safety runner is missing; build or reinstall @hasna/hooks before installing its guards.");
|
|
2827
|
+
const canonicalWorker = realpathSync2(worker);
|
|
2828
|
+
return buildNativeSafetyCommand({ name, runtime, runtimeSHA256: pinEligibleFile(runtime, 200000000, true), worker: canonicalWorker, workerSHA256: pinEligibleFile(canonicalWorker, 256000), home, path: `${dirname3(runtime)}:${join5(home, ".bun/bin")}:/usr/local/bin:/usr/bin:/bin` });
|
|
2829
|
+
}
|
|
2830
|
+
|
|
2699
2831
|
// src/lib/registration.ts
|
|
2700
2832
|
init_resolve();
|
|
2701
2833
|
|
|
@@ -2760,6 +2892,9 @@ var REGISTRATION_COMMAND_RE = /^(?:hooks run ([\w-]+)(?:\s+--profile\s+[\w-]+)?|
|
|
|
2760
2892
|
function registrationName(command) {
|
|
2761
2893
|
if (typeof command !== "string")
|
|
2762
2894
|
return;
|
|
2895
|
+
const native = nativeSafetyRegistration(command);
|
|
2896
|
+
if (native)
|
|
2897
|
+
return native;
|
|
2763
2898
|
if (/^hooks run mementos-context(?:\s|$)/.test(command.trim())) {
|
|
2764
2899
|
try {
|
|
2765
2900
|
retainedMementosRegistration([command.trim()]);
|
|
@@ -2814,8 +2949,7 @@ function hookRegisteredInSettings(settings, name, eventSpec, hookMatcher) {
|
|
|
2814
2949
|
if (!Array.isArray(hooks))
|
|
2815
2950
|
return false;
|
|
2816
2951
|
return hooks.some((entry) => entry?.hooks?.some((h) => {
|
|
2817
|
-
|
|
2818
|
-
if (!match || match[1] !== name)
|
|
2952
|
+
if (registrationName(h?.command) !== name)
|
|
2819
2953
|
return false;
|
|
2820
2954
|
if (!matcher)
|
|
2821
2955
|
return true;
|
|
@@ -2827,13 +2961,13 @@ function hookRegisteredInSettings(settings, name, eventSpec, hookMatcher) {
|
|
|
2827
2961
|
}
|
|
2828
2962
|
|
|
2829
2963
|
// src/lib/store.ts
|
|
2830
|
-
import { createHash } from "crypto";
|
|
2831
|
-
import { existsSync as
|
|
2964
|
+
import { createHash as createHash2 } from "crypto";
|
|
2965
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync3, readFileSync as readFileSync4, writeFileSync as writeFileSync2, renameSync } from "fs";
|
|
2832
2966
|
init_db();
|
|
2833
2967
|
init_config();
|
|
2834
2968
|
import { randomBytes } from "crypto";
|
|
2835
2969
|
function sha256Of(input) {
|
|
2836
|
-
return
|
|
2970
|
+
return createHash2("sha256").update(input).digest("hex");
|
|
2837
2971
|
}
|
|
2838
2972
|
function storeDb() {
|
|
2839
2973
|
return isLocalStoreRefused() ? null : getDb();
|
|
@@ -2889,11 +3023,11 @@ function assertLockShape(parsed, path) {
|
|
|
2889
3023
|
}
|
|
2890
3024
|
function readLock() {
|
|
2891
3025
|
const path = getLockPath();
|
|
2892
|
-
if (!
|
|
3026
|
+
if (!existsSync7(path))
|
|
2893
3027
|
return { hooks: {} };
|
|
2894
3028
|
let raw;
|
|
2895
3029
|
try {
|
|
2896
|
-
raw =
|
|
3030
|
+
raw = readFileSync4(path, "utf-8");
|
|
2897
3031
|
} catch {
|
|
2898
3032
|
throw new LockFileError(`hooks.lock (${path}) could not be read. Repair: fix permissions or move the file aside and run 'hooks sync'.`);
|
|
2899
3033
|
}
|
|
@@ -2965,16 +3099,16 @@ function checkScriptHash(name, actual) {
|
|
|
2965
3099
|
}
|
|
2966
3100
|
|
|
2967
3101
|
// src/lib/codex-settings.ts
|
|
2968
|
-
import { closeSync, existsSync as
|
|
2969
|
-
import { dirname as
|
|
3102
|
+
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";
|
|
3103
|
+
import { dirname as dirname4 } from "path";
|
|
2970
3104
|
import { randomUUID } from "crypto";
|
|
2971
3105
|
var snapshots = new WeakMap;
|
|
2972
3106
|
function current(path) {
|
|
2973
3107
|
try {
|
|
2974
|
-
const stat =
|
|
3108
|
+
const stat = lstatSync2(path);
|
|
2975
3109
|
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 1048576)
|
|
2976
3110
|
throw new Error("invalid_file");
|
|
2977
|
-
return
|
|
3111
|
+
return readFileSync5(path, "utf8");
|
|
2978
3112
|
} catch (error) {
|
|
2979
3113
|
if (error.code === "ENOENT")
|
|
2980
3114
|
return null;
|
|
@@ -3006,11 +3140,11 @@ function writeCodexSettings(path, settings) {
|
|
|
3006
3140
|
const snapshot = snapshots.get(settings);
|
|
3007
3141
|
if (!snapshot || snapshot.path !== path)
|
|
3008
3142
|
throw new Error("Codex settings require a complete current read before writing.");
|
|
3009
|
-
mkdirSync4(
|
|
3143
|
+
mkdirSync4(dirname4(path), { recursive: true });
|
|
3010
3144
|
const lock = `${path}.hooks-write.lock`;
|
|
3011
3145
|
let fd;
|
|
3012
3146
|
try {
|
|
3013
|
-
fd =
|
|
3147
|
+
fd = openSync2(lock, "wx", 384);
|
|
3014
3148
|
} catch {
|
|
3015
3149
|
throw new Error("Codex settings are being edited; retry after the active writer finishes.");
|
|
3016
3150
|
}
|
|
@@ -3026,16 +3160,16 @@ function writeCodexSettings(path, settings) {
|
|
|
3026
3160
|
renameSync2(temp, path);
|
|
3027
3161
|
snapshots.set(settings, { path, raw: next });
|
|
3028
3162
|
} finally {
|
|
3029
|
-
if (
|
|
3163
|
+
if (existsSync8(temp))
|
|
3030
3164
|
unlinkSync(temp);
|
|
3031
|
-
|
|
3165
|
+
closeSync2(fd);
|
|
3032
3166
|
unlinkSync(lock);
|
|
3033
3167
|
}
|
|
3034
3168
|
}
|
|
3035
3169
|
|
|
3036
3170
|
// src/lib/installer.ts
|
|
3037
|
-
var __dirname2 =
|
|
3038
|
-
var HOOKS_DIR =
|
|
3171
|
+
var __dirname2 = dirname5(fileURLToPath2(import.meta.url));
|
|
3172
|
+
var HOOKS_DIR = existsSync9(join8(__dirname2, "..", "..", "hooks", "hook-gitguard")) ? join8(__dirname2, "..", "..", "hooks") : join8(__dirname2, "..", "hooks");
|
|
3039
3173
|
function normalizeHookName(name) {
|
|
3040
3174
|
return name.startsWith("hook-") ? name : `hook-${name}`;
|
|
3041
3175
|
}
|
|
@@ -3047,8 +3181,7 @@ function removeHookEntriesByName(entries, hookName) {
|
|
|
3047
3181
|
if (!Array.isArray(entry.hooks))
|
|
3048
3182
|
return [entry];
|
|
3049
3183
|
const kept = entry.hooks.filter((h) => {
|
|
3050
|
-
|
|
3051
|
-
return !(match && match[1] === hookName);
|
|
3184
|
+
return registrationName(h.command) !== hookName;
|
|
3052
3185
|
});
|
|
3053
3186
|
return kept.length === entry.hooks.length ? [entry] : kept.length ? [{ ...entry, hooks: kept }] : [];
|
|
3054
3187
|
});
|
|
@@ -3124,29 +3257,29 @@ function getSettingsPath(scope = "global", target = "claude", codewithConfigPath
|
|
|
3124
3257
|
return globalOverride;
|
|
3125
3258
|
const dir = getTargetSettingsDir(target);
|
|
3126
3259
|
if (target === "codex")
|
|
3127
|
-
return
|
|
3260
|
+
return join8(scope === "project" ? process.cwd() : homedir4(), dir, "hooks.json");
|
|
3128
3261
|
if (scope === "project") {
|
|
3129
|
-
return target === "codewith" ?
|
|
3262
|
+
return target === "codewith" ? join8(process.cwd(), dir, "config.toml") : join8(process.cwd(), dir, "settings.json");
|
|
3130
3263
|
}
|
|
3131
|
-
return target === "codewith" ?
|
|
3264
|
+
return target === "codewith" ? join8(homedir4(), dir, "config.toml") : join8(homedir4(), dir, "settings.json");
|
|
3132
3265
|
}
|
|
3133
3266
|
function getHookPath(name) {
|
|
3134
3267
|
const shortName = shortHookName(name);
|
|
3135
3268
|
const resolved = resolveHookDir(shortName);
|
|
3136
3269
|
if (resolved)
|
|
3137
3270
|
return resolved;
|
|
3138
|
-
return
|
|
3271
|
+
return join8(HOOKS_DIR, normalizeHookName(shortName));
|
|
3139
3272
|
}
|
|
3140
3273
|
function hookExists(name) {
|
|
3141
|
-
return
|
|
3274
|
+
return existsSync9(getHookPath(name));
|
|
3142
3275
|
}
|
|
3143
3276
|
function readSettings(scope = "global", target = "claude") {
|
|
3144
3277
|
const path = getSettingsPath(scope, target);
|
|
3145
3278
|
if (target === "codex")
|
|
3146
3279
|
return readCodexSettings(path);
|
|
3147
3280
|
try {
|
|
3148
|
-
if (
|
|
3149
|
-
return JSON.parse(
|
|
3281
|
+
if (existsSync9(path)) {
|
|
3282
|
+
return JSON.parse(readFileSync6(path, "utf-8"));
|
|
3150
3283
|
}
|
|
3151
3284
|
} catch (error) {
|
|
3152
3285
|
console.warn(`[hooks] Failed to read settings at ${path}: ${error instanceof Error ? error.message : String(error)}`);
|
|
@@ -3157,8 +3290,8 @@ function writeSettings(settings, scope = "global", target = "claude") {
|
|
|
3157
3290
|
const path = getSettingsPath(scope, target);
|
|
3158
3291
|
if (target === "codex")
|
|
3159
3292
|
return writeCodexSettings(path, settings);
|
|
3160
|
-
const dir =
|
|
3161
|
-
if (!
|
|
3293
|
+
const dir = dirname5(path);
|
|
3294
|
+
if (!existsSync9(dir)) {
|
|
3162
3295
|
mkdirSync5(dir, { recursive: true });
|
|
3163
3296
|
}
|
|
3164
3297
|
writeFileSync4(path, JSON.stringify(settings, null, 2) + `
|
|
@@ -3186,6 +3319,9 @@ function codewithTimeout(name) {
|
|
|
3186
3319
|
switch (name) {
|
|
3187
3320
|
case "mementos-context":
|
|
3188
3321
|
return 12;
|
|
3322
|
+
case "trash-guard":
|
|
3323
|
+
case "workspace-repos-guard":
|
|
3324
|
+
return 10;
|
|
3189
3325
|
case "knowledge-context":
|
|
3190
3326
|
return 6;
|
|
3191
3327
|
case "session-start":
|
|
@@ -3195,7 +3331,6 @@ function codewithTimeout(name) {
|
|
|
3195
3331
|
case "prompt-guard":
|
|
3196
3332
|
return 3;
|
|
3197
3333
|
case "worktree-guard":
|
|
3198
|
-
case "trash-guard":
|
|
3199
3334
|
case "stop-sync":
|
|
3200
3335
|
return 5;
|
|
3201
3336
|
default:
|
|
@@ -3224,11 +3359,13 @@ function codewithStatusMessage(name) {
|
|
|
3224
3359
|
}
|
|
3225
3360
|
function buildCodewithTomlFragment(name, profile, mementos) {
|
|
3226
3361
|
const shortName = shortHookName(name);
|
|
3227
|
-
|
|
3362
|
+
if (isNativeSafetyName(shortName) && profile)
|
|
3363
|
+
throw new Error("Bundled native safety guards do not select registry profiles");
|
|
3364
|
+
const meta = isNativeSafetyName(shortName) ? getHook(shortName) : resolveHookMeta(shortName);
|
|
3228
3365
|
if (!meta)
|
|
3229
3366
|
throw new Error(`Hook '${shortName}' not found`);
|
|
3230
3367
|
validateMementosRegistration(shortName, mementos);
|
|
3231
|
-
const command = (profile ? `hooks run ${shortName} --profile ${profile}` : `hooks run ${shortName}`) + mementosCommandSuffix(mementos);
|
|
3368
|
+
const command = isNativeSafetyName(shortName) ? installedNativeSafetyCommand(shortName) : (profile ? `hooks run ${shortName} --profile ${profile}` : `hooks run ${shortName}`) + mementosCommandSuffix(mementos);
|
|
3232
3369
|
const matcher = codewithMatcher(meta.matcher);
|
|
3233
3370
|
const fragments = [];
|
|
3234
3371
|
for (const event of getHookEvents(meta)) {
|
|
@@ -3253,17 +3390,17 @@ function buildCodewithTomlFragment(name, profile, mementos) {
|
|
|
3253
3390
|
function readCodewithConfig(scope, configPath) {
|
|
3254
3391
|
const path = getSettingsPath(scope, "codewith", configPath);
|
|
3255
3392
|
try {
|
|
3256
|
-
return
|
|
3393
|
+
return existsSync9(path) ? readFileSync6(path, "utf-8") : "";
|
|
3257
3394
|
} catch {
|
|
3258
3395
|
return "";
|
|
3259
3396
|
}
|
|
3260
3397
|
}
|
|
3261
3398
|
function appendCodewithFragment(fragment, scope, configPath) {
|
|
3262
3399
|
const path = getSettingsPath(scope, "codewith", configPath);
|
|
3263
|
-
const dir =
|
|
3264
|
-
if (!
|
|
3400
|
+
const dir = dirname5(path);
|
|
3401
|
+
if (!existsSync9(dir))
|
|
3265
3402
|
mkdirSync5(dir, { recursive: true });
|
|
3266
|
-
const existing =
|
|
3403
|
+
const existing = existsSync9(path) ? readFileSync6(path, "utf-8") : "";
|
|
3267
3404
|
const sep2 = existing.trim() ? `
|
|
3268
3405
|
|
|
3269
3406
|
` : "";
|
|
@@ -3271,7 +3408,7 @@ function appendCodewithFragment(fragment, scope, configPath) {
|
|
|
3271
3408
|
return path;
|
|
3272
3409
|
}
|
|
3273
3410
|
function detectConflict(name, scope, target) {
|
|
3274
|
-
const meta = resolveHookMeta(name);
|
|
3411
|
+
const meta = isNativeSafetyName(name) ? getHook(name) : resolveHookMeta(name);
|
|
3275
3412
|
if (!meta || !meta.matcher)
|
|
3276
3413
|
return;
|
|
3277
3414
|
const events = new Set(getHookEvents(meta));
|
|
@@ -3291,7 +3428,7 @@ function detectConflict(name, scope, target) {
|
|
|
3291
3428
|
return;
|
|
3292
3429
|
}
|
|
3293
3430
|
function detectRewriteConflict(name, scope, target, lookup = getHook) {
|
|
3294
|
-
const meta = resolveHookMeta(name);
|
|
3431
|
+
const meta = isNativeSafetyName(name) ? getHook(name) : resolveHookMeta(name);
|
|
3295
3432
|
if (!meta?.matcher || !meta.rewritesInput)
|
|
3296
3433
|
return;
|
|
3297
3434
|
if (!getHookEvents(meta).includes("PreToolUse"))
|
|
@@ -3312,7 +3449,10 @@ function detectRewriteConflict(name, scope, target, lookup = getHook) {
|
|
|
3312
3449
|
}
|
|
3313
3450
|
function installForTarget(name, scope, overwrite, target, profile, codewithMode = "fragment", codewithConfigPath, mementos) {
|
|
3314
3451
|
const shortName = shortHookName(name);
|
|
3315
|
-
if (
|
|
3452
|
+
if (isNativeSafetyName(shortName) && (profile || target === "gemini" || target === "codewith" && codewithMode === "write")) {
|
|
3453
|
+
return { hook: shortName, success: false, error: "Bundled native safety guards support Claude/Codex and managed Codewith fragments, without registry profiles.", target };
|
|
3454
|
+
}
|
|
3455
|
+
if (!isNativeSafetyName(shortName) && !hookExists(shortName)) {
|
|
3316
3456
|
return { hook: shortName, success: false, error: `Hook '${shortName}' not found`, target };
|
|
3317
3457
|
}
|
|
3318
3458
|
if (target === "codewith") {
|
|
@@ -3404,7 +3544,7 @@ function installHook(name, options = {}) {
|
|
|
3404
3544
|
}
|
|
3405
3545
|
if (target === "all") {
|
|
3406
3546
|
const shortName = shortHookName(name);
|
|
3407
|
-
const meta = resolveHookMeta(shortName);
|
|
3547
|
+
const meta = isNativeSafetyName(shortName) ? getHook(shortName) : resolveHookMeta(shortName);
|
|
3408
3548
|
if (meta) {
|
|
3409
3549
|
const unsupportedTargets = ["claude", "codewith"].filter((agentTarget) => getHookEvents(meta).some((event) => !isEventSupported(event, agentTarget)));
|
|
3410
3550
|
if (unsupportedTargets.length > 0) {
|
|
@@ -3437,7 +3577,7 @@ function installHook(name, options = {}) {
|
|
|
3437
3577
|
return installForTarget(name, scope, overwrite, target, profile, codewithMode, codewithConfigPath, mementos);
|
|
3438
3578
|
}
|
|
3439
3579
|
function registerHook(name, scope = "global", target = "claude", profile, mementos) {
|
|
3440
|
-
const meta = resolveHookMeta(name);
|
|
3580
|
+
const meta = isNativeSafetyName(name) ? getHook(name) : resolveHookMeta(name);
|
|
3441
3581
|
if (!meta)
|
|
3442
3582
|
return;
|
|
3443
3583
|
const eventKeys = getHookEvents(meta).map((event) => {
|
|
@@ -3454,8 +3594,17 @@ function registerHook(name, scope = "global", target = "claude", profile, mement
|
|
|
3454
3594
|
const settings = readSettings(scope, target);
|
|
3455
3595
|
if (!settings.hooks)
|
|
3456
3596
|
settings.hooks = {};
|
|
3597
|
+
if (isNativeSafetyName(name)) {
|
|
3598
|
+
for (const groups of Object.values(settings.hooks))
|
|
3599
|
+
for (const group of groups)
|
|
3600
|
+
for (const hook of group.hooks ?? []) {
|
|
3601
|
+
if (typeof hook.command === "string" && /(?:^|\s)hasna-native-safety-v\d+(?:\s|$)/.test(hook.command) && !registrationName(hook.command)) {
|
|
3602
|
+
throw new Error("Unknown managed safety definition; reconcile its exact version before replacing it.");
|
|
3603
|
+
}
|
|
3604
|
+
}
|
|
3605
|
+
}
|
|
3457
3606
|
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;
|
|
3458
|
-
const hookCommand = (profile ? `hooks run ${name} --profile ${profile}` : `hooks run ${name}`) + mementosCommandSuffix(mementos ?? retained);
|
|
3607
|
+
const hookCommand = isNativeSafetyName(name) ? installedNativeSafetyCommand(name) : (profile ? `hooks run ${name} --profile ${profile}` : `hooks run ${name}`) + mementosCommandSuffix(mementos ?? retained);
|
|
3459
3608
|
if (target === "codex") {
|
|
3460
3609
|
const positions = [];
|
|
3461
3610
|
for (const [event, groups] of Object.entries(settings.hooks)) {
|
|
@@ -3463,9 +3612,11 @@ function registerHook(name, scope = "global", target = "claude", profile, mement
|
|
|
3463
3612
|
for (let index = 0;index < entry.hooks.length; index++) {
|
|
3464
3613
|
const command = entry.hooks[index]?.command;
|
|
3465
3614
|
const match = typeof command === "string" && command.trim().match(/^(?:hooks run ([\w-]+)(?:\s|$)|hook-([\w-]+)$)/);
|
|
3466
|
-
if (match && (match[1] ?? match[2]) === name) {
|
|
3615
|
+
if (match && (match[1] ?? match[2]) === name || registrationName(command) === name) {
|
|
3467
3616
|
if (registrationName(command) !== name)
|
|
3468
3617
|
throw new Error("Unrecognized Codex registration; reconcile it before updating this hook.");
|
|
3618
|
+
if (isNativeSafetyName(name) && entry.hooks[index].async !== undefined && entry.hooks[index].async !== false)
|
|
3619
|
+
throw new Error("Asynchronous safety hooks cannot block; reconcile this registration before updating it.");
|
|
3469
3620
|
positions.push({ event, entry, index });
|
|
3470
3621
|
}
|
|
3471
3622
|
}
|
|
@@ -3526,12 +3677,20 @@ function getRegisteredHooksForTarget(scope = "global", target = "claude") {
|
|
|
3526
3677
|
if (target === "codewith") {
|
|
3527
3678
|
const config = readCodewithConfig(scope);
|
|
3528
3679
|
const registered2 = [];
|
|
3529
|
-
|
|
3530
|
-
|
|
3531
|
-
|
|
3532
|
-
|
|
3533
|
-
|
|
3534
|
-
|
|
3680
|
+
try {
|
|
3681
|
+
const parsed = Bun.TOML.parse(config);
|
|
3682
|
+
for (const groups of Object.values(parsed.hooks ?? {})) {
|
|
3683
|
+
if (!Array.isArray(groups))
|
|
3684
|
+
continue;
|
|
3685
|
+
for (const group of groups)
|
|
3686
|
+
for (const hook of group.hooks ?? []) {
|
|
3687
|
+
const name = registrationName(hook.command);
|
|
3688
|
+
if (name)
|
|
3689
|
+
registered2.push(name);
|
|
3690
|
+
}
|
|
3691
|
+
}
|
|
3692
|
+
} catch {
|
|
3693
|
+
return [];
|
|
3535
3694
|
}
|
|
3536
3695
|
return [...new Set(registered2)];
|
|
3537
3696
|
}
|
|
@@ -3577,12 +3736,12 @@ function removeHookForTarget(name, scope, target) {
|
|
|
3577
3736
|
|
|
3578
3737
|
// src/lib/profiles.ts
|
|
3579
3738
|
init_app_home();
|
|
3580
|
-
import { existsSync as
|
|
3581
|
-
import { join as
|
|
3739
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync6, readFileSync as readFileSync7, writeFileSync as writeFileSync5, readdirSync as readdirSync4, rmSync, cpSync as cpSync2 } from "fs";
|
|
3740
|
+
import { join as join9 } from "path";
|
|
3582
3741
|
function getProfilesDir() {
|
|
3583
|
-
const newDir =
|
|
3584
|
-
const oldDir =
|
|
3585
|
-
if (!
|
|
3742
|
+
const newDir = join9(getEffectiveDataRoot(), "profiles");
|
|
3743
|
+
const oldDir = join9(getHomeDir(), ".hooks", "profiles");
|
|
3744
|
+
if (!existsSync10(newDir) && existsSync10(oldDir)) {
|
|
3586
3745
|
mkdirSync6(newDir, { recursive: true });
|
|
3587
3746
|
cpSync2(oldDir, newDir, { recursive: true });
|
|
3588
3747
|
}
|
|
@@ -3590,12 +3749,12 @@ function getProfilesDir() {
|
|
|
3590
3749
|
}
|
|
3591
3750
|
function ensureProfilesDir() {
|
|
3592
3751
|
const dir = getProfilesDir();
|
|
3593
|
-
if (!
|
|
3752
|
+
if (!existsSync10(dir)) {
|
|
3594
3753
|
mkdirSync6(dir, { recursive: true });
|
|
3595
3754
|
}
|
|
3596
3755
|
}
|
|
3597
3756
|
function profilePath(id) {
|
|
3598
|
-
return
|
|
3757
|
+
return join9(getProfilesDir(), `${id}.json`);
|
|
3599
3758
|
}
|
|
3600
3759
|
function shortUuid() {
|
|
3601
3760
|
return crypto.randomUUID().slice(0, 8);
|
|
@@ -3621,23 +3780,23 @@ function createProfile(input) {
|
|
|
3621
3780
|
function getProfile(id) {
|
|
3622
3781
|
const path = profilePath(id);
|
|
3623
3782
|
try {
|
|
3624
|
-
if (!
|
|
3783
|
+
if (!existsSync10(path))
|
|
3625
3784
|
return null;
|
|
3626
|
-
return JSON.parse(
|
|
3785
|
+
return JSON.parse(readFileSync7(path, "utf-8"));
|
|
3627
3786
|
} catch {
|
|
3628
3787
|
return null;
|
|
3629
3788
|
}
|
|
3630
3789
|
}
|
|
3631
3790
|
function listProfiles() {
|
|
3632
3791
|
const dir = getProfilesDir();
|
|
3633
|
-
if (!
|
|
3792
|
+
if (!existsSync10(dir))
|
|
3634
3793
|
return [];
|
|
3635
3794
|
try {
|
|
3636
3795
|
const files = readdirSync4(dir).filter((f) => f.endsWith(".json"));
|
|
3637
3796
|
const profiles = [];
|
|
3638
3797
|
for (const file of files) {
|
|
3639
3798
|
try {
|
|
3640
|
-
const content =
|
|
3799
|
+
const content = readFileSync7(join9(dir, file), "utf-8");
|
|
3641
3800
|
profiles.push(JSON.parse(content));
|
|
3642
3801
|
} catch {}
|
|
3643
3802
|
}
|
|
@@ -4172,13 +4331,13 @@ function coerceForSqlite(value) {
|
|
|
4172
4331
|
return String(value);
|
|
4173
4332
|
}
|
|
4174
4333
|
// src/mcp/server.ts
|
|
4175
|
-
var __dirname3 =
|
|
4334
|
+
var __dirname3 = dirname8(fileURLToPath3(import.meta.url));
|
|
4176
4335
|
var pkg = { name: "@hasna/hooks", version: "0.0.0" };
|
|
4177
4336
|
try {
|
|
4178
4337
|
for (const rel of ["../../package.json", "../package.json", "../../../package.json"]) {
|
|
4179
|
-
const p =
|
|
4180
|
-
if (
|
|
4181
|
-
pkg = JSON.parse(
|
|
4338
|
+
const p = join11(__dirname3, rel);
|
|
4339
|
+
if (existsSync12(p)) {
|
|
4340
|
+
pkg = JSON.parse(readFileSync8(p, "utf-8"));
|
|
4182
4341
|
break;
|
|
4183
4342
|
}
|
|
4184
4343
|
}
|
|
@@ -4187,10 +4346,10 @@ async function runVerifiedHook(name, stdin, timeoutMs) {
|
|
|
4187
4346
|
const { resolveHook: resolveHook2, resolveScriptPath: resolveScriptPath2 } = await Promise.resolve().then(() => (init_resolve(), exports_resolve));
|
|
4188
4347
|
const resolved = resolveHook2(name);
|
|
4189
4348
|
const hookScript = resolveScriptPath2(name);
|
|
4190
|
-
if (!resolved || !hookScript || !
|
|
4349
|
+
if (!resolved || !hookScript || !existsSync12(hookScript)) {
|
|
4191
4350
|
throw new Error(`Hook '${name}' not found`);
|
|
4192
4351
|
}
|
|
4193
|
-
const content =
|
|
4352
|
+
const content = readFileSync8(hookScript);
|
|
4194
4353
|
const sha = sha256Of(content);
|
|
4195
4354
|
const check = checkScriptHash(name, sha);
|
|
4196
4355
|
if (!check.ok) {
|
|
@@ -4396,7 +4555,7 @@ function createHooksServer() {
|
|
|
4396
4555
|
const settingsPath = getSettingsPath(scope);
|
|
4397
4556
|
const issues = [];
|
|
4398
4557
|
const healthy = [];
|
|
4399
|
-
const settingsExist =
|
|
4558
|
+
const settingsExist = existsSync12(settingsPath);
|
|
4400
4559
|
if (!settingsExist) {
|
|
4401
4560
|
issues.push({ hook: "(settings)", issue: `${settingsPath} not found`, severity: "warning" });
|
|
4402
4561
|
}
|
|
@@ -4406,7 +4565,7 @@ function createHooksServer() {
|
|
|
4406
4565
|
const meta = custom ? resolveHookMeta(name) : getHook(name);
|
|
4407
4566
|
let hookHealthy = true;
|
|
4408
4567
|
if (custom) {
|
|
4409
|
-
if (!
|
|
4568
|
+
if (!existsSync12(custom.scriptPath)) {
|
|
4410
4569
|
issues.push({ hook: name, issue: `Custom hook script missing: ${custom.scriptPath}`, severity: "error" });
|
|
4411
4570
|
hookHealthy = false;
|
|
4412
4571
|
}
|
|
@@ -4416,14 +4575,14 @@ function createHooksServer() {
|
|
|
4416
4575
|
continue;
|
|
4417
4576
|
}
|
|
4418
4577
|
const hookDir = getHookPath(name);
|
|
4419
|
-
if (!
|
|
4578
|
+
if (!existsSync12(join11(hookDir, "src", "hook.ts"))) {
|
|
4420
4579
|
issues.push({ hook: name, issue: "Missing src/hook.ts in package", severity: "error" });
|
|
4421
4580
|
hookHealthy = false;
|
|
4422
4581
|
}
|
|
4423
4582
|
}
|
|
4424
4583
|
if (meta && settingsExist) {
|
|
4425
4584
|
try {
|
|
4426
|
-
const settings = JSON.parse(
|
|
4585
|
+
const settings = JSON.parse(readFileSync8(settingsPath, "utf-8"));
|
|
4427
4586
|
if (!hookRegisteredInSettings(settings, name, meta.event, meta.matcher)) {
|
|
4428
4587
|
const eventName = meta.event.split(":")[0];
|
|
4429
4588
|
issues.push({ hook: name, issue: `Not registered under correct event (${eventName})`, severity: "error" });
|
|
@@ -4460,10 +4619,10 @@ function createHooksServer() {
|
|
|
4460
4619
|
return { content: [{ type: "text", text: JSON.stringify({ error: `Hook '${name}' not found` }) }] };
|
|
4461
4620
|
}
|
|
4462
4621
|
const hookPath = getHookPath(name);
|
|
4463
|
-
const readmePath =
|
|
4622
|
+
const readmePath = join11(hookPath, "README.md");
|
|
4464
4623
|
let readme = "";
|
|
4465
|
-
if (
|
|
4466
|
-
readme =
|
|
4624
|
+
if (existsSync12(readmePath)) {
|
|
4625
|
+
readme = readFileSync8(readmePath, "utf-8");
|
|
4467
4626
|
}
|
|
4468
4627
|
if (verbose) {
|
|
4469
4628
|
return { content: [{ type: "text", text: JSON.stringify({ ...meta, readme }) }] };
|
|
@@ -4646,7 +4805,7 @@ function createHooksServer() {
|
|
|
4646
4805
|
const ctx = {
|
|
4647
4806
|
scope,
|
|
4648
4807
|
settings_path: settingsPath,
|
|
4649
|
-
settings_exists:
|
|
4808
|
+
settings_exists: existsSync12(settingsPath),
|
|
4650
4809
|
registered_hooks: hooks,
|
|
4651
4810
|
hook_count: hooks.length,
|
|
4652
4811
|
healthy,
|
|
@@ -4792,8 +4951,8 @@ function createHooksServer() {
|
|
|
4792
4951
|
const settingsPath = getSettingsPath(scope);
|
|
4793
4952
|
let settings = {};
|
|
4794
4953
|
try {
|
|
4795
|
-
if (
|
|
4796
|
-
settings = JSON.parse(
|
|
4954
|
+
if (existsSync12(settingsPath))
|
|
4955
|
+
settings = JSON.parse(readFileSync8(settingsPath, "utf-8"));
|
|
4797
4956
|
} catch {}
|
|
4798
4957
|
if (!settings.hooks)
|
|
4799
4958
|
settings.hooks = {};
|
|
@@ -4802,8 +4961,8 @@ function createHooksServer() {
|
|
|
4802
4961
|
disabled.push(name);
|
|
4803
4962
|
settings.hooks.__disabled = disabled;
|
|
4804
4963
|
const { writeFileSync: writeFileSync7, mkdirSync: mkdirSync7 } = await import("fs");
|
|
4805
|
-
const { dirname:
|
|
4806
|
-
mkdirSync7(
|
|
4964
|
+
const { dirname: dirname9 } = await import("path");
|
|
4965
|
+
mkdirSync7(dirname9(settingsPath), { recursive: true });
|
|
4807
4966
|
writeFileSync7(settingsPath, JSON.stringify(settings, null, 2) + `
|
|
4808
4967
|
`);
|
|
4809
4968
|
return { content: [{ type: "text", text: JSON.stringify({ hook: name, disabled: true, scope }) }] };
|
|
@@ -4815,8 +4974,8 @@ function createHooksServer() {
|
|
|
4815
4974
|
const settingsPath = getSettingsPath(scope);
|
|
4816
4975
|
let settings = {};
|
|
4817
4976
|
try {
|
|
4818
|
-
if (
|
|
4819
|
-
settings = JSON.parse(
|
|
4977
|
+
if (existsSync12(settingsPath))
|
|
4978
|
+
settings = JSON.parse(readFileSync8(settingsPath, "utf-8"));
|
|
4820
4979
|
} catch {}
|
|
4821
4980
|
if (settings.hooks?.__disabled) {
|
|
4822
4981
|
settings.hooks.__disabled = settings.hooks.__disabled.filter((n) => n !== name);
|