@hasna/hooks 0.10.6 → 0.10.7
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 +43 -0
- package/bin/hooks-mcp.js +48 -25
- package/bin/index.js +7647 -6918
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1030 -49
- 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/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -6202,8 +6202,8 @@ var init_db_writer = __esm(() => {
|
|
|
6202
6202
|
});
|
|
6203
6203
|
|
|
6204
6204
|
// src/lib/hook-env.ts
|
|
6205
|
-
import { dirname as
|
|
6206
|
-
import { existsSync as
|
|
6205
|
+
import { dirname as dirname24, isAbsolute as isAbsolute21 } from "path";
|
|
6206
|
+
import { existsSync as existsSync14, realpathSync as realpathSync13, statSync as statSync2 } from "fs";
|
|
6207
6207
|
import { tmpdir as tmpdir2 } from "os";
|
|
6208
6208
|
function isDeniedEnvName(name) {
|
|
6209
6209
|
const upper = name.toUpperCase();
|
|
@@ -6222,7 +6222,7 @@ function isBashFunctionEnvName(name) {
|
|
|
6222
6222
|
return name.startsWith("BASH_FUNC_");
|
|
6223
6223
|
}
|
|
6224
6224
|
function isUnsafePathEntry(entry, home) {
|
|
6225
|
-
if (entry === "" || !
|
|
6225
|
+
if (entry === "" || !isAbsolute21(entry))
|
|
6226
6226
|
return true;
|
|
6227
6227
|
if (home && (entry === home || entry.startsWith(home + "/")))
|
|
6228
6228
|
return true;
|
|
@@ -6230,16 +6230,16 @@ function isUnsafePathEntry(entry, home) {
|
|
|
6230
6230
|
return true;
|
|
6231
6231
|
if (entry === "/var/tmp" || entry.startsWith("/var/tmp/"))
|
|
6232
6232
|
return true;
|
|
6233
|
-
const
|
|
6233
|
+
const canonical5 = (path) => {
|
|
6234
6234
|
try {
|
|
6235
|
-
return
|
|
6235
|
+
return realpathSync13(path);
|
|
6236
6236
|
} catch {
|
|
6237
6237
|
return path;
|
|
6238
6238
|
}
|
|
6239
6239
|
};
|
|
6240
|
-
const resolved =
|
|
6240
|
+
const resolved = canonical5(entry);
|
|
6241
6241
|
for (const root of [home, tmpdir2(), "/tmp", "/var/tmp"].filter((value) => Boolean(value))) {
|
|
6242
|
-
const blocked =
|
|
6242
|
+
const blocked = canonical5(root);
|
|
6243
6243
|
if (resolved === blocked || resolved.startsWith(blocked + "/"))
|
|
6244
6244
|
return true;
|
|
6245
6245
|
}
|
|
@@ -6251,9 +6251,9 @@ function isUnsafePathEntry(entry, home) {
|
|
|
6251
6251
|
}
|
|
6252
6252
|
function sanitizeHookPath(pathValue, home) {
|
|
6253
6253
|
const baseline = PATH_BASELINE.slice();
|
|
6254
|
-
if (process.platform === "darwin" &&
|
|
6254
|
+
if (process.platform === "darwin" && existsSync14("/opt/homebrew/bin"))
|
|
6255
6255
|
baseline.push("/opt/homebrew/bin");
|
|
6256
|
-
const bunDir =
|
|
6256
|
+
const bunDir = dirname24(process.execPath);
|
|
6257
6257
|
const seen = new Set;
|
|
6258
6258
|
const out = [];
|
|
6259
6259
|
for (const dir of baseline) {
|
|
@@ -6377,10 +6377,10 @@ __export(exports_run, {
|
|
|
6377
6377
|
HookTimeoutError: () => HookTimeoutError
|
|
6378
6378
|
});
|
|
6379
6379
|
import { randomBytes as randomBytes2 } from "crypto";
|
|
6380
|
-
import { rmSync as rmSync4, writeFileSync as
|
|
6381
|
-
import { basename as
|
|
6380
|
+
import { rmSync as rmSync4, writeFileSync as writeFileSync13 } from "fs";
|
|
6381
|
+
import { basename as basename11, dirname as dirname25, isAbsolute as isAbsolute23, join as join30 } from "path";
|
|
6382
6382
|
function extensionOf(scriptPath) {
|
|
6383
|
-
const base =
|
|
6383
|
+
const base = basename11(scriptPath);
|
|
6384
6384
|
const dot = base.lastIndexOf(".");
|
|
6385
6385
|
return dot > 0 ? base.slice(dot + 1) : "ts";
|
|
6386
6386
|
}
|
|
@@ -6392,9 +6392,9 @@ function shebangInterpreter(firstLine) {
|
|
|
6392
6392
|
return null;
|
|
6393
6393
|
const parts = rest.split(/\s+/);
|
|
6394
6394
|
if (parts[0] === "env" || parts[0] === "/usr/bin/env") {
|
|
6395
|
-
return parts[1] && parts[1] !== "-S" ?
|
|
6395
|
+
return parts[1] && parts[1] !== "-S" ? basename11(parts[1]) : parts[2] ? basename11(parts[2]) : null;
|
|
6396
6396
|
}
|
|
6397
|
-
return
|
|
6397
|
+
return basename11(parts[0]);
|
|
6398
6398
|
}
|
|
6399
6399
|
function interpreterFor(name, scriptPath, content) {
|
|
6400
6400
|
const ext = extensionOf(scriptPath);
|
|
@@ -6447,9 +6447,9 @@ async function readPipeWithDeadline(stream, deadlineMs) {
|
|
|
6447
6447
|
return chunks.join("");
|
|
6448
6448
|
}
|
|
6449
6449
|
async function executeVerifiedScript(options) {
|
|
6450
|
-
const scriptDir =
|
|
6450
|
+
const scriptDir = dirname25(options.scriptPath);
|
|
6451
6451
|
const interpreter = interpreterFor(options.name, options.scriptPath, options.content);
|
|
6452
|
-
const tempPath =
|
|
6452
|
+
const tempPath = join30(scriptDir, `.hooks-verified-${randomBytes2(12).toString("hex")}.${interpreter.tempExt}`);
|
|
6453
6453
|
let proc = null;
|
|
6454
6454
|
let timedOut = false;
|
|
6455
6455
|
const timeoutTimer = options.timeout ? setTimeout(() => {
|
|
@@ -6457,8 +6457,8 @@ async function executeVerifiedScript(options) {
|
|
|
6457
6457
|
killGroup(proc, { leaderAlive: proc !== null && proc.exitCode === null });
|
|
6458
6458
|
}, options.timeout) : null;
|
|
6459
6459
|
try {
|
|
6460
|
-
|
|
6461
|
-
proc = Bun.spawn([...interpreter.command,
|
|
6460
|
+
writeFileSync13(tempPath, options.content, { flag: "wx", mode: 384 });
|
|
6461
|
+
proc = Bun.spawn([...interpreter.command, isAbsolute23(tempPath) ? tempPath : `./${tempPath}`, ...options.args ?? []], {
|
|
6462
6462
|
stdin: new Response(options.stdin),
|
|
6463
6463
|
stdout: "pipe",
|
|
6464
6464
|
stderr: "pipe",
|
|
@@ -7854,7 +7854,7 @@ init_store();
|
|
|
7854
7854
|
// src/lib/codex-settings.ts
|
|
7855
7855
|
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";
|
|
7856
7856
|
import { dirname as dirname4 } from "path";
|
|
7857
|
-
import { randomUUID } from "crypto";
|
|
7857
|
+
import { createHash as createHash3, randomUUID } from "crypto";
|
|
7858
7858
|
var snapshots = new WeakMap;
|
|
7859
7859
|
function current(path) {
|
|
7860
7860
|
try {
|
|
@@ -7889,11 +7889,30 @@ function readCodexSettings(path) {
|
|
|
7889
7889
|
snapshots.set(settings, { path, raw });
|
|
7890
7890
|
return settings;
|
|
7891
7891
|
}
|
|
7892
|
-
function
|
|
7892
|
+
function capturedCodexSettings(settings) {
|
|
7893
|
+
const snapshot = snapshots.get(settings);
|
|
7894
|
+
if (!snapshot)
|
|
7895
|
+
throw new Error("Hook settings require a complete current read.");
|
|
7896
|
+
return snapshot.raw;
|
|
7897
|
+
}
|
|
7898
|
+
function assertSettingsPreimage(raw, expected) {
|
|
7899
|
+
if (expected === undefined)
|
|
7900
|
+
return;
|
|
7901
|
+
if (!/^(?:absent|[a-f0-9]{64})$/.test(expected) || (raw === null ? "absent" : createHash3("sha256").update(raw).digest("hex")) !== expected) {
|
|
7902
|
+
throw new Error("Hook settings changed since planning; no settings were written.");
|
|
7903
|
+
}
|
|
7904
|
+
}
|
|
7905
|
+
function writeCodexSettings(path, settings, expectedSha256) {
|
|
7893
7906
|
const snapshot = snapshots.get(settings);
|
|
7894
7907
|
if (!snapshot || snapshot.path !== path)
|
|
7895
7908
|
throw new Error("Codex settings require a complete current read before writing.");
|
|
7896
|
-
|
|
7909
|
+
assertSettingsPreimage(snapshot.raw, expectedSha256);
|
|
7910
|
+
writeHookSettingsPreimage(path, settings, snapshot.raw);
|
|
7911
|
+
snapshots.set(settings, { path, raw: JSON.stringify(settings, null, 2) + `
|
|
7912
|
+
` });
|
|
7913
|
+
}
|
|
7914
|
+
function writeHookSettingsPreimage(path, settings, before) {
|
|
7915
|
+
mkdirSync4(dirname4(path), { recursive: true, mode: 448 });
|
|
7897
7916
|
const lock = `${path}.hooks-write.lock`;
|
|
7898
7917
|
let fd;
|
|
7899
7918
|
try {
|
|
@@ -7903,15 +7922,14 @@ function writeCodexSettings(path, settings) {
|
|
|
7903
7922
|
}
|
|
7904
7923
|
const temp = `${path}.${randomUUID()}.tmp`;
|
|
7905
7924
|
try {
|
|
7906
|
-
if (current(path) !==
|
|
7907
|
-
throw new Error("
|
|
7925
|
+
if (current(path) !== before)
|
|
7926
|
+
throw new Error("Hook settings changed since reading; re-read before retrying.");
|
|
7908
7927
|
const next = JSON.stringify(settings, null, 2) + `
|
|
7909
7928
|
`;
|
|
7910
7929
|
writeFileSync3(temp, next, { flag: "wx", mode: 384 });
|
|
7911
|
-
if (current(path) !==
|
|
7912
|
-
throw new Error("
|
|
7930
|
+
if (current(path) !== before)
|
|
7931
|
+
throw new Error("Hook settings changed since reading; re-read before retrying.");
|
|
7913
7932
|
renameSync2(temp, path);
|
|
7914
|
-
snapshots.set(settings, { path, raw: next });
|
|
7915
7933
|
} finally {
|
|
7916
7934
|
if (existsSync8(temp))
|
|
7917
7935
|
unlinkSync(temp);
|
|
@@ -7929,7 +7947,7 @@ import { homedir as homedir4 } from "os";
|
|
|
7929
7947
|
import { join as join8, resolve as resolve3 } from "path";
|
|
7930
7948
|
import { homedir as pathsResolverHomedir2 } from "os";
|
|
7931
7949
|
import { join as pathsResolverJoin2 } from "path";
|
|
7932
|
-
import { createHash as
|
|
7950
|
+
import { createHash as createHash4 } from "crypto";
|
|
7933
7951
|
import { existsSync as existsSync52, readdirSync as readdirSync3, readFileSync as readFileSync42, statSync as statSync3 } from "fs";
|
|
7934
7952
|
import { join as join52, sep as sep2 } from "path";
|
|
7935
7953
|
import { basename as basename2, isAbsolute as isAbsolute22, resolve as resolve22 } from "path";
|
|
@@ -7944,7 +7962,7 @@ import { dirname as dirname42, isAbsolute as isAbsolute4, join as join82, resolv
|
|
|
7944
7962
|
import { createHash as createHash32 } from "crypto";
|
|
7945
7963
|
import { closeSync as closeSync3, constants as constants2, fstatSync as fstatSync2, lstatSync as lstatSync5, openSync as openSync3, readSync } from "fs";
|
|
7946
7964
|
import { dirname as dirname5, isAbsolute as isAbsolute5, resolve as resolve5 } from "path";
|
|
7947
|
-
import { createHash as
|
|
7965
|
+
import { createHash as createHash42 } from "crypto";
|
|
7948
7966
|
import { lstatSync as lstatSync6, readlinkSync } from "fs";
|
|
7949
7967
|
import { dirname as dirname6, isAbsolute as isAbsolute6, join as join9, parse, resolve as resolve6, sep as sep22 } from "path";
|
|
7950
7968
|
import { closeSync as closeSync12, constants as constants9, fstatSync as fstatSync12, lstatSync as lstatSync17, openSync as openSync12, opendirSync as opendirSync5, readSync as readSync8 } from "fs";
|
|
@@ -15072,7 +15090,7 @@ function* bundleHashParts(files) {
|
|
|
15072
15090
|
yield new TextEncoder().encode("\x00");
|
|
15073
15091
|
}
|
|
15074
15092
|
function hashBundleFiles(files) {
|
|
15075
|
-
const hash =
|
|
15093
|
+
const hash = createHash4(CONTENT_HASH_ALGORITHM);
|
|
15076
15094
|
for (const part of bundleHashParts(files))
|
|
15077
15095
|
hash.update(part);
|
|
15078
15096
|
return hash.digest("hex");
|
|
@@ -15908,7 +15926,7 @@ function hashDiscoveryPathFile(path, budget, changes) {
|
|
|
15908
15926
|
const after = trace(path);
|
|
15909
15927
|
if (JSON.stringify(before.rows) !== JSON.stringify(after.rows) || before.file !== after.file)
|
|
15910
15928
|
throw new Error("Discovery path input changed during read");
|
|
15911
|
-
return
|
|
15929
|
+
return createHash42("sha256").update(JSON.stringify({ version: 1, path, rows: before.rows, content })).digest("hex");
|
|
15912
15930
|
}
|
|
15913
15931
|
var package_default = {
|
|
15914
15932
|
name: "@hasna/skills",
|
|
@@ -39139,9 +39157,9 @@ function getTargetSettingsDir(target) {
|
|
|
39139
39157
|
}
|
|
39140
39158
|
function getGlobalSettingsPathOverride(target) {
|
|
39141
39159
|
if (target === "codex")
|
|
39142
|
-
return process.env.HASNA_HOOKS_CODEX_SETTINGS_PATH;
|
|
39160
|
+
return process.env.HASNA_HOOKS_CODEX_SETTINGS_PATH ?? (process.env.CODEX_HOME ? resolve7(process.env.CODEX_HOME, "hooks.json") : undefined);
|
|
39143
39161
|
if (target === "claude")
|
|
39144
|
-
return process.env.HASNA_HOOKS_CLAUDE_SETTINGS_PATH;
|
|
39162
|
+
return process.env.HASNA_HOOKS_CLAUDE_SETTINGS_PATH ?? (process.env.CLAUDE_CONFIG_DIR ? resolve7(process.env.CLAUDE_CONFIG_DIR, "settings.json") : undefined);
|
|
39145
39163
|
if (target === "gemini")
|
|
39146
39164
|
return process.env.HASNA_HOOKS_GEMINI_SETTINGS_PATH;
|
|
39147
39165
|
return;
|
|
@@ -39185,7 +39203,7 @@ function readSettings2(scope = "global", target = "claude") {
|
|
|
39185
39203
|
return value;
|
|
39186
39204
|
}
|
|
39187
39205
|
} catch (error61) {
|
|
39188
|
-
console.warn(
|
|
39206
|
+
console.warn("[hooks] Cannot read hook settings; repair the selected file before installation.");
|
|
39189
39207
|
}
|
|
39190
39208
|
const empty = {};
|
|
39191
39209
|
if (!existsSync10(path))
|
|
@@ -39197,10 +39215,16 @@ function settingsIdentity(path) {
|
|
|
39197
39215
|
return realpathSync5(path);
|
|
39198
39216
|
return existsSync10(dirname7(path)) ? join10(realpathSync5(dirname7(path)), basename3(path)) : resolve7(path);
|
|
39199
39217
|
}
|
|
39200
|
-
function writeSettings(settings, scope = "global", target = "claude") {
|
|
39218
|
+
function writeSettings(settings, scope = "global", target = "claude", expectedSettingsSha256) {
|
|
39201
39219
|
const path = getSettingsPath(scope, target);
|
|
39202
39220
|
if (target === "codex")
|
|
39203
|
-
return writeCodexSettings(path, settings);
|
|
39221
|
+
return writeCodexSettings(path, settings, expectedSettingsSha256);
|
|
39222
|
+
const captured = settingsPreimages.get(settings);
|
|
39223
|
+
if (expectedSettingsSha256 !== undefined) {
|
|
39224
|
+
if (!captured || captured.path !== path)
|
|
39225
|
+
throw new Error("Hook settings require a valid captured preimage.");
|
|
39226
|
+
assertSettingsPreimage(captured.text, expectedSettingsSha256);
|
|
39227
|
+
}
|
|
39204
39228
|
if (target === "claude" && settingsIdentity(path) === settingsIdentity(join10(homedir5(), ".claude/settings.json"))) {
|
|
39205
39229
|
const before = settingsPreimages.get(settings);
|
|
39206
39230
|
if (!before || before.path !== path)
|
|
@@ -39212,6 +39236,8 @@ function writeSettings(settings, scope = "global", target = "claude") {
|
|
|
39212
39236
|
`, { home: homedir5() }))
|
|
39213
39237
|
return;
|
|
39214
39238
|
}
|
|
39239
|
+
if (expectedSettingsSha256 !== undefined)
|
|
39240
|
+
return writeHookSettingsPreimage(path, settings, captured.text);
|
|
39215
39241
|
const dir = dirname7(path);
|
|
39216
39242
|
if (!existsSync10(dir)) {
|
|
39217
39243
|
mkdirSync6(dir, { recursive: true });
|
|
@@ -39369,7 +39395,7 @@ function detectRewriteConflict(name, scope, target, lookup = getHook) {
|
|
|
39369
39395
|
}
|
|
39370
39396
|
return;
|
|
39371
39397
|
}
|
|
39372
|
-
function installForTarget(name, scope, overwrite, target, profile, codewithMode = "fragment", codewithConfigPath, mementos) {
|
|
39398
|
+
function installForTarget(name, scope, overwrite, target, profile, codewithMode = "fragment", codewithConfigPath, mementos, expectedSettingsSha256) {
|
|
39373
39399
|
const shortName = shortHookName(name);
|
|
39374
39400
|
if (isNativeSafetyName(shortName) && (profile || target === "gemini" || target === "codewith" && codewithMode === "write")) {
|
|
39375
39401
|
return { hook: shortName, success: false, error: "Bundled native safety guards support Claude/Codex and managed Codewith fragments, without registry profiles.", target };
|
|
@@ -39436,7 +39462,7 @@ function installForTarget(name, scope, overwrite, target, profile, codewithMode
|
|
|
39436
39462
|
if (rewriteConflict) {
|
|
39437
39463
|
return { hook: shortName, success: false, error: `Refused: ${rewriteConflict}`, scope, target };
|
|
39438
39464
|
}
|
|
39439
|
-
registerHook(shortName, scope, target, profile, mementos);
|
|
39465
|
+
registerHook(shortName, scope, target, profile, mementos, (settings, selectedScope, selectedTarget) => writeSettings(settings, selectedScope, selectedTarget, expectedSettingsSha256));
|
|
39440
39466
|
return {
|
|
39441
39467
|
hook: shortName,
|
|
39442
39468
|
success: true,
|
|
@@ -39455,7 +39481,10 @@ function installForTarget(name, scope, overwrite, target, profile, codewithMode
|
|
|
39455
39481
|
}
|
|
39456
39482
|
}
|
|
39457
39483
|
function installHook(name, options = {}) {
|
|
39458
|
-
const { scope = "global", overwrite = false, target = "claude", profile, codewithMode = "fragment", codewithConfigPath, mementos } = options;
|
|
39484
|
+
const { scope = "global", overwrite = false, target = "claude", profile, codewithMode = "fragment", codewithConfigPath, mementos, expectedSettingsSha256 } = options;
|
|
39485
|
+
if (expectedSettingsSha256 !== undefined && (!/^(?:absent|[a-f0-9]{64})$/.test(expectedSettingsSha256) || !isNativeSafetyName(shortHookName(name)) || target !== "codex" && target !== "claude")) {
|
|
39486
|
+
return { hook: name, success: false, target, error: "An exact settings predecessor requires a Claude or Codex native safety guard." };
|
|
39487
|
+
}
|
|
39459
39488
|
try {
|
|
39460
39489
|
validateMementosRegistration(name, mementos);
|
|
39461
39490
|
} catch (error61) {
|
|
@@ -39496,9 +39525,26 @@ function installHook(name, options = {}) {
|
|
|
39496
39525
|
}
|
|
39497
39526
|
return { ...claudeResult, target: "all" };
|
|
39498
39527
|
}
|
|
39499
|
-
return installForTarget(name, scope, overwrite, target, profile, codewithMode, codewithConfigPath, mementos);
|
|
39528
|
+
return installForTarget(name, scope, overwrite, target, profile, codewithMode, codewithConfigPath, mementos, expectedSettingsSha256);
|
|
39500
39529
|
}
|
|
39501
|
-
function
|
|
39530
|
+
function previewNativeSafetyRegistration(target, scope = "global") {
|
|
39531
|
+
const conflict = detectRewriteConflict("trash-guard", scope, target);
|
|
39532
|
+
if (conflict)
|
|
39533
|
+
throw new Error("Conflicting input rewriter; reconcile it before installing the safety guard.");
|
|
39534
|
+
let result;
|
|
39535
|
+
registerHook("trash-guard", scope, target, undefined, undefined, (settings) => {
|
|
39536
|
+
const before = target === "codex" ? capturedCodexSettings(settings) : settingsPreimages.get(settings)?.text;
|
|
39537
|
+
if (before === undefined)
|
|
39538
|
+
throw new Error("Hook settings require a valid captured preimage.");
|
|
39539
|
+
const command = installedNativeSafetyCommand("trash-guard");
|
|
39540
|
+
result = { before, after: JSON.stringify(settings, null, 2) + `
|
|
39541
|
+
`, command };
|
|
39542
|
+
});
|
|
39543
|
+
if (!result)
|
|
39544
|
+
throw new Error("Native safety registration could not be rendered.");
|
|
39545
|
+
return result;
|
|
39546
|
+
}
|
|
39547
|
+
function registerHook(name, scope = "global", target = "claude", profile, mementos, write = writeSettings) {
|
|
39502
39548
|
const meta3 = isNativeSafetyName(name) ? getHook(name) : resolveHookMeta(name);
|
|
39503
39549
|
if (!meta3)
|
|
39504
39550
|
return;
|
|
@@ -39527,7 +39573,7 @@ function registerHook(name, scope = "global", target = "claude", profile, mement
|
|
|
39527
39573
|
}
|
|
39528
39574
|
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;
|
|
39529
39575
|
const hookCommand = isNativeSafetyName(name) ? installedNativeSafetyCommand(name) : (profile ? `hooks run ${name} --profile ${profile}` : `hooks run ${name}`) + mementosCommandSuffix(mementos ?? retained);
|
|
39530
|
-
if (target === "codex") {
|
|
39576
|
+
if (target === "codex" || target === "claude" && isNativeSafetyName(name)) {
|
|
39531
39577
|
const positions = [];
|
|
39532
39578
|
for (const [event, groups] of Object.entries(settings.hooks)) {
|
|
39533
39579
|
for (const entry of groups) {
|
|
@@ -39553,7 +39599,7 @@ function registerHook(name, scope = "global", target = "claude", profile, mement
|
|
|
39553
39599
|
if (typeof meta3.timeoutSeconds === "number" && meta3.timeoutSeconds > 0)
|
|
39554
39600
|
entry.hooks[index].timeout = meta3.timeoutSeconds;
|
|
39555
39601
|
}
|
|
39556
|
-
|
|
39602
|
+
write(settings, scope, target);
|
|
39557
39603
|
return;
|
|
39558
39604
|
}
|
|
39559
39605
|
}
|
|
@@ -39573,7 +39619,7 @@ function registerHook(name, scope = "global", target = "claude", profile, mement
|
|
|
39573
39619
|
}
|
|
39574
39620
|
settings.hooks[eventKey].push(entry);
|
|
39575
39621
|
}
|
|
39576
|
-
|
|
39622
|
+
write(settings, scope, target);
|
|
39577
39623
|
}
|
|
39578
39624
|
function removeHookFromAllEvents(settings, name) {
|
|
39579
39625
|
if (!settings.hooks)
|
|
@@ -39807,7 +39853,7 @@ function uninstallHook(name, scope = "global", target = "claude") {
|
|
|
39807
39853
|
// src/index.ts
|
|
39808
39854
|
init_profiles();
|
|
39809
39855
|
init_config();
|
|
39810
|
-
import { existsSync as
|
|
39856
|
+
import { existsSync as existsSync15, readFileSync as readFileSync26 } from "fs";
|
|
39811
39857
|
|
|
39812
39858
|
// src/lib/transport.ts
|
|
39813
39859
|
init_transport();
|
|
@@ -40046,16 +40092,16 @@ async function installManifestUrl(url2) {
|
|
|
40046
40092
|
return { name, version: manifest.version, kind: "url", source: url2, dir, scriptPath };
|
|
40047
40093
|
}
|
|
40048
40094
|
// src/lib/sync.ts
|
|
40049
|
-
import { createHash as
|
|
40095
|
+
import { createHash as createHash6 } from "crypto";
|
|
40050
40096
|
import { readFileSync as readFileSync15 } from "fs";
|
|
40051
40097
|
init_db();
|
|
40052
40098
|
init_store();
|
|
40053
40099
|
init_manifest();
|
|
40054
40100
|
function sha256OfText(text3) {
|
|
40055
|
-
return
|
|
40101
|
+
return createHash6("sha256").update(text3).digest("hex");
|
|
40056
40102
|
}
|
|
40057
40103
|
function sha256OfPath(path) {
|
|
40058
|
-
return
|
|
40104
|
+
return createHash6("sha256").update(readFileSync15(path)).digest("hex");
|
|
40059
40105
|
}
|
|
40060
40106
|
function collectBundledCatalog() {
|
|
40061
40107
|
const out = [];
|
|
@@ -40977,6 +41023,933 @@ async function provisionCloudflareResources(options) {
|
|
|
40977
41023
|
];
|
|
40978
41024
|
return result;
|
|
40979
41025
|
}
|
|
41026
|
+
// src/lib/codex-safety-trust.ts
|
|
41027
|
+
import { createHash as createHash12, randomUUID as randomUUID3 } from "crypto";
|
|
41028
|
+
import { constants as constants12, closeSync as closeSync7, fsyncSync as fsyncSync5, fstatSync as fstatSync6, lstatSync as lstatSync15, mkdirSync as mkdirSync13, openSync as openSync7, readFileSync as readFileSync21, realpathSync as realpathSync10, writeFileSync as writeFileSync11 } from "fs";
|
|
41029
|
+
import { dirname as dirname21, isAbsolute as isAbsolute18, join as join24, relative as relative2 } from "path";
|
|
41030
|
+
import { homedir as homedir7 } from "os";
|
|
41031
|
+
|
|
41032
|
+
// src/lib/codex-safety-check.ts
|
|
41033
|
+
import { spawn, execFile as execFile2 } from "child_process";
|
|
41034
|
+
import { createHash as createHash7 } from "crypto";
|
|
41035
|
+
import { constants as constants4, openSync as openSync6, closeSync as closeSync6, fstatSync as fstatSync5, lstatSync as lstatSync10, readFileSync as readFileSync20, realpathSync as realpathSync9 } from "fs";
|
|
41036
|
+
import { dirname as dirname20, isAbsolute as isAbsolute17, resolve as resolve17 } from "path";
|
|
41037
|
+
import { promisify } from "util";
|
|
41038
|
+
|
|
41039
|
+
// src/native-safety.ts
|
|
41040
|
+
import { execFile } from "child_process";
|
|
41041
|
+
import { closeSync as closeSync5, constants as constants3, existsSync as existsSync13, fstatSync as fstatSync4, fsyncSync, linkSync as linkSync3, lstatSync as lstatSync9, mkdirSync as mkdirSync12, openSync as openSync5, readFileSync as readFileSync19, realpathSync as realpathSync8, renameSync as renameSync3, unlinkSync as unlinkSync5, writeFileSync as writeFileSync10 } from "fs";
|
|
41042
|
+
import { homedir as homedir6 } from "os";
|
|
41043
|
+
import { dirname as dirname19, isAbsolute as isAbsolute15, join as join20, resolve as resolve15 } from "path";
|
|
41044
|
+
class NativeSafetyError extends Error {
|
|
41045
|
+
code;
|
|
41046
|
+
reason;
|
|
41047
|
+
constructor(code2, reason) {
|
|
41048
|
+
super(`Native deletion safety refused (${code2}). Run the reviewed Hooks safety setup before using shell or patch tools.`);
|
|
41049
|
+
this.code = code2;
|
|
41050
|
+
this.reason = reason;
|
|
41051
|
+
this.name = "NativeSafetyError";
|
|
41052
|
+
}
|
|
41053
|
+
}
|
|
41054
|
+
var executionSchema = "hasna.hooks.execution.v1";
|
|
41055
|
+
var owner2 = (uid2) => uid2 === 0 || uid2 === process.getuid?.();
|
|
41056
|
+
function safeDirectory(path) {
|
|
41057
|
+
for (let cursor = path;; cursor = dirname19(cursor)) {
|
|
41058
|
+
const stat2 = lstatSync9(cursor);
|
|
41059
|
+
if (!stat2.isDirectory() || !owner2(stat2.uid) || stat2.mode & 18)
|
|
41060
|
+
throw new NativeSafetyError("binding_invalid");
|
|
41061
|
+
if (dirname19(cursor) === cursor)
|
|
41062
|
+
return;
|
|
41063
|
+
}
|
|
41064
|
+
}
|
|
41065
|
+
function canonicalHome(home) {
|
|
41066
|
+
if (!isAbsolute15(home) || home.includes("\x00") || realpathSync8(home) !== resolve15(home))
|
|
41067
|
+
throw new NativeSafetyError("binding_invalid");
|
|
41068
|
+
safeDirectory(home);
|
|
41069
|
+
return resolve15(home);
|
|
41070
|
+
}
|
|
41071
|
+
function parseRecord(text3, home) {
|
|
41072
|
+
const record3 = JSON.parse(text3);
|
|
41073
|
+
if (!record3 || Array.isArray(record3) || Object.keys(record3).sort().join() !== "command,harness,schema" || record3.schema !== "hasna.hooks.native-safety.v1" || record3.harness !== "sumi" || typeof record3.command !== "string")
|
|
41074
|
+
throw new NativeSafetyError("binding_invalid");
|
|
41075
|
+
const binding = nativeSafetyCommandBinding(record3.command);
|
|
41076
|
+
if (binding?.name !== "trash-guard" || binding.home !== home)
|
|
41077
|
+
throw new NativeSafetyError("binding_invalid");
|
|
41078
|
+
return record3;
|
|
41079
|
+
}
|
|
41080
|
+
async function evaluateRecord(event, record3, signal, executionAgent) {
|
|
41081
|
+
if (!event || event.hook_event_name !== "PreToolUse" || !["Bash", "apply_patch"].includes(event.tool_name) || typeof event.cwd !== "string" || !isAbsolute15(event.cwd) || !event.tool_input || typeof event.tool_input !== "object" || Array.isArray(event.tool_input))
|
|
41082
|
+
throw new NativeSafetyError("input_invalid");
|
|
41083
|
+
const input2 = JSON.stringify(executionAgent === undefined ? event : { ...event, hasna_execution: { schema: executionSchema, agent: executionAgent } });
|
|
41084
|
+
if (Buffer.byteLength(input2) > 1048576)
|
|
41085
|
+
throw new NativeSafetyError("input_invalid");
|
|
41086
|
+
const output2 = await new Promise((accept, reject) => {
|
|
41087
|
+
const child = execFile("/bin/sh", ["-c", record3.command], { cwd: "/", env: {}, encoding: "utf8", timeout: 8000, maxBuffer: 1048576, signal }, (error61, stdout, stderr) => {
|
|
41088
|
+
if (error61 || stderr) {
|
|
41089
|
+
reject(new NativeSafetyError("guard_failed"));
|
|
41090
|
+
return;
|
|
41091
|
+
}
|
|
41092
|
+
accept(stdout);
|
|
41093
|
+
});
|
|
41094
|
+
child.stdin?.on("error", () => {});
|
|
41095
|
+
child.stdin?.end(input2);
|
|
41096
|
+
});
|
|
41097
|
+
if (!output2)
|
|
41098
|
+
return { decision: "continue" };
|
|
41099
|
+
try {
|
|
41100
|
+
const value = JSON.parse(output2);
|
|
41101
|
+
if (Object.keys(value).join() !== "hookSpecificOutput")
|
|
41102
|
+
throw new Error;
|
|
41103
|
+
const verdict = value.hookSpecificOutput;
|
|
41104
|
+
if (verdict.hookEventName !== "PreToolUse")
|
|
41105
|
+
throw new Error;
|
|
41106
|
+
if (verdict.permissionDecision === "deny" && Object.keys(verdict).sort().join() === "hookEventName,permissionDecision,permissionDecisionReason" && typeof verdict.permissionDecisionReason === "string" && verdict.permissionDecisionReason.trim())
|
|
41107
|
+
return { decision: "deny", reason: verdict.permissionDecisionReason };
|
|
41108
|
+
if (event.tool_name === "Bash" && verdict.permissionDecision === "allow" && Object.keys(verdict).sort().join() === "hookEventName,permissionDecision,updatedInput" && typeof verdict.updatedInput?.command === "string" && verdict.updatedInput.command.trim() && (executionAgent === undefined || verdict.updatedInput.hasna_execution === executionSchema))
|
|
41109
|
+
return { decision: "rewrite", command: verdict.updatedInput.command };
|
|
41110
|
+
} catch {}
|
|
41111
|
+
throw new NativeSafetyError("guard_failed");
|
|
41112
|
+
}
|
|
41113
|
+
async function verifyNativeSafetyCommand(command, home = homedir6()) {
|
|
41114
|
+
home = canonicalHome(home);
|
|
41115
|
+
const record3 = parseRecord(JSON.stringify({ schema: "hasna.hooks.native-safety.v1", harness: "sumi", command }), home);
|
|
41116
|
+
const verdict = await evaluateRecord({ hook_event_name: "PreToolUse", tool_name: "Bash", cwd: home, tool_input: { command: "rm -- .hasna-trash-verification-only" } }, record3);
|
|
41117
|
+
if (verdict.decision !== "rewrite")
|
|
41118
|
+
throw new NativeSafetyError("guard_failed", verdict.decision === "deny" ? verdict.reason.slice(0, 512) : undefined);
|
|
41119
|
+
}
|
|
41120
|
+
|
|
41121
|
+
// src/lib/codex-safety-check.ts
|
|
41122
|
+
var versions = new Set(["codex-cli 0.153.0", "codex-cli 0.154.0", "codex-cli 0.155.0", "codex-cli 0.155.1", "codex-cli 0.156.1"]);
|
|
41123
|
+
var matcher = "^(Bash|apply_patch|ApplyPatch|functions\\.apply_patch)$";
|
|
41124
|
+
|
|
41125
|
+
class CodexSafetyError extends Error {
|
|
41126
|
+
code;
|
|
41127
|
+
constructor(code2) {
|
|
41128
|
+
super(`Codex deletion safety check failed (${code2}).`);
|
|
41129
|
+
this.code = code2;
|
|
41130
|
+
this.name = "CodexSafetyError";
|
|
41131
|
+
}
|
|
41132
|
+
}
|
|
41133
|
+
function need5(condition, code2) {
|
|
41134
|
+
if (!condition)
|
|
41135
|
+
throw new CodexSafetyError(code2);
|
|
41136
|
+
}
|
|
41137
|
+
var digest3 = (bytes) => createHash7("sha256").update(bytes).digest("hex");
|
|
41138
|
+
function nativeHeader(bytes) {
|
|
41139
|
+
if (bytes.length < 64 || !["arm64", "x64"].includes(process.arch))
|
|
41140
|
+
return false;
|
|
41141
|
+
if (process.platform === "darwin") {
|
|
41142
|
+
const cpu = process.arch === "arm64" ? 16777228 : 16777223;
|
|
41143
|
+
return bytes.readUInt32LE(0) === 4277009103 && bytes.readUInt32LE(4) === cpu && bytes.readUInt32LE(12) === 2 && bytes.readUInt32LE(16) > 0 && bytes.readUInt32LE(16) <= 4096 && bytes.readUInt32LE(20) > 0 && bytes.readUInt32LE(20) <= bytes.length - 32;
|
|
41144
|
+
}
|
|
41145
|
+
if (process.platform === "linux") {
|
|
41146
|
+
const offset = bytes.readBigUInt64LE(32), count = bytes.readUInt16LE(56);
|
|
41147
|
+
return bytes.subarray(0, 7).equals(Buffer.from([127, 69, 76, 70, 2, 1, 1])) && [2, 3].includes(bytes.readUInt16LE(16)) && bytes.readUInt16LE(18) === (process.arch === "arm64" ? 183 : 62) && bytes.readUInt32LE(20) === 1 && bytes.readUInt16LE(52) === 64 && bytes.readUInt16LE(54) === 56 && count > 0 && count <= 1024 && offset >= 64n && offset + BigInt(count * 56) <= BigInt(bytes.length);
|
|
41148
|
+
}
|
|
41149
|
+
return false;
|
|
41150
|
+
}
|
|
41151
|
+
function executable(command) {
|
|
41152
|
+
const found = isAbsolute17(command) ? command : Bun.which(command);
|
|
41153
|
+
need5(found, "native_unavailable");
|
|
41154
|
+
try {
|
|
41155
|
+
const path = realpathSync9(found);
|
|
41156
|
+
for (let parent = dirname20(path);; parent = dirname20(parent)) {
|
|
41157
|
+
const stat2 = lstatSync10(parent);
|
|
41158
|
+
need5(stat2.isDirectory() && [0, process.getuid?.()].includes(stat2.uid) && !(stat2.mode & 18), "native_unsafe_executable");
|
|
41159
|
+
if (parent === dirname20(parent))
|
|
41160
|
+
break;
|
|
41161
|
+
}
|
|
41162
|
+
const fd = openSync6(path, constants4.O_RDONLY | constants4.O_NOFOLLOW | constants4.O_NONBLOCK);
|
|
41163
|
+
try {
|
|
41164
|
+
const stat2 = fstatSync5(fd);
|
|
41165
|
+
need5(stat2.isFile() && [0, process.getuid?.()].includes(stat2.uid) && !(stat2.mode & 18) && !!(stat2.mode & 64) && stat2.size <= 256000000, "native_unsafe_executable");
|
|
41166
|
+
const bytes = readFileSync20(fd);
|
|
41167
|
+
need5(bytes.length <= 256000000, "native_unavailable");
|
|
41168
|
+
need5(nativeHeader(bytes), "native_executable_required");
|
|
41169
|
+
return { path, sha256: digest3(bytes) };
|
|
41170
|
+
} finally {
|
|
41171
|
+
closeSync6(fd);
|
|
41172
|
+
}
|
|
41173
|
+
} catch (error61) {
|
|
41174
|
+
if (error61 instanceof CodexSafetyError)
|
|
41175
|
+
throw error61;
|
|
41176
|
+
throw new CodexSafetyError("native_unavailable");
|
|
41177
|
+
}
|
|
41178
|
+
}
|
|
41179
|
+
async function connectCodexSafety(options) {
|
|
41180
|
+
const command = options.command ?? "codex", before = executable(command);
|
|
41181
|
+
const env = { ...process.env, HOME: options.home, NO_COLOR: "1" };
|
|
41182
|
+
for (const key of ["NODE_OPTIONS", "NODE_PATH", "BUN_OPTIONS", "BASH_ENV", "ENV", "DYLD_INSERT_LIBRARIES", "LD_PRELOAD"])
|
|
41183
|
+
delete env[key];
|
|
41184
|
+
if (options.codexHome === undefined)
|
|
41185
|
+
delete env.CODEX_HOME;
|
|
41186
|
+
else
|
|
41187
|
+
env.CODEX_HOME = options.codexHome;
|
|
41188
|
+
let version2;
|
|
41189
|
+
try {
|
|
41190
|
+
version2 = (await promisify(execFile2)(before.path, ["--version"], { env, cwd: options.home, timeout: 5000, killSignal: "SIGKILL", maxBuffer: 4096 })).stdout.trim();
|
|
41191
|
+
} catch {
|
|
41192
|
+
throw new CodexSafetyError("native_unavailable");
|
|
41193
|
+
}
|
|
41194
|
+
need5(versions.has(version2), "native_unsupported");
|
|
41195
|
+
need5(JSON.stringify(executable(command)) === JSON.stringify(before), "guard_changed");
|
|
41196
|
+
const child = spawn(before.path, ["app-server", "--strict-config", "--stdio"], { env, cwd: options.home, stdio: ["pipe", "pipe", "pipe"] });
|
|
41197
|
+
let stopped = false, buffer = Buffer.alloc(0), total = 0, nextId = 1;
|
|
41198
|
+
const pending = new Map;
|
|
41199
|
+
const fail = () => {
|
|
41200
|
+
stopped = true;
|
|
41201
|
+
for (const waiter of pending.values())
|
|
41202
|
+
waiter.reject(new CodexSafetyError("native_failed"));
|
|
41203
|
+
pending.clear();
|
|
41204
|
+
};
|
|
41205
|
+
const deadline = setTimeout(() => {
|
|
41206
|
+
fail();
|
|
41207
|
+
child.kill("SIGKILL");
|
|
41208
|
+
}, 15000);
|
|
41209
|
+
const exited = new Promise((done) => {
|
|
41210
|
+
child.once("exit", () => {
|
|
41211
|
+
fail();
|
|
41212
|
+
clearTimeout(deadline);
|
|
41213
|
+
done();
|
|
41214
|
+
});
|
|
41215
|
+
child.once("error", () => {
|
|
41216
|
+
fail();
|
|
41217
|
+
clearTimeout(deadline);
|
|
41218
|
+
done();
|
|
41219
|
+
});
|
|
41220
|
+
});
|
|
41221
|
+
child.stdin.on("error", fail);
|
|
41222
|
+
const account = (length) => {
|
|
41223
|
+
total += length;
|
|
41224
|
+
if (total > 8388608) {
|
|
41225
|
+
fail();
|
|
41226
|
+
child.kill("SIGKILL");
|
|
41227
|
+
return false;
|
|
41228
|
+
}
|
|
41229
|
+
return true;
|
|
41230
|
+
};
|
|
41231
|
+
child.stderr.on("data", (bytes) => {
|
|
41232
|
+
account(bytes.length);
|
|
41233
|
+
});
|
|
41234
|
+
child.stdout.on("data", (bytes) => {
|
|
41235
|
+
if (stopped || !account(bytes.length))
|
|
41236
|
+
return;
|
|
41237
|
+
buffer = Buffer.concat([buffer, bytes]);
|
|
41238
|
+
for (;; ) {
|
|
41239
|
+
const end = buffer.indexOf(10);
|
|
41240
|
+
if (end < 0)
|
|
41241
|
+
return;
|
|
41242
|
+
const line = buffer.subarray(0, end);
|
|
41243
|
+
buffer = buffer.subarray(end + 1);
|
|
41244
|
+
try {
|
|
41245
|
+
const value = JSON.parse(line.toString("utf8"));
|
|
41246
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
41247
|
+
throw Error();
|
|
41248
|
+
if (!Object.hasOwn(value, "id"))
|
|
41249
|
+
continue;
|
|
41250
|
+
const waiter = pending.get(value.id);
|
|
41251
|
+
if (!waiter)
|
|
41252
|
+
throw Error();
|
|
41253
|
+
pending.delete(value.id);
|
|
41254
|
+
if (value.error || !Object.hasOwn(value, "result"))
|
|
41255
|
+
waiter.reject(new CodexSafetyError("native_failed"));
|
|
41256
|
+
else
|
|
41257
|
+
waiter.resolve(value.result);
|
|
41258
|
+
} catch {
|
|
41259
|
+
fail();
|
|
41260
|
+
child.kill("SIGKILL");
|
|
41261
|
+
return;
|
|
41262
|
+
}
|
|
41263
|
+
}
|
|
41264
|
+
});
|
|
41265
|
+
const request = (method, params) => new Promise((accept, reject) => {
|
|
41266
|
+
if (stopped) {
|
|
41267
|
+
reject(new CodexSafetyError("native_failed"));
|
|
41268
|
+
return;
|
|
41269
|
+
}
|
|
41270
|
+
const id = nextId++;
|
|
41271
|
+
pending.set(id, { resolve: accept, reject });
|
|
41272
|
+
child.stdin.write(JSON.stringify({ id, method, params }) + `
|
|
41273
|
+
`);
|
|
41274
|
+
});
|
|
41275
|
+
const rpc = {
|
|
41276
|
+
version: version2,
|
|
41277
|
+
request,
|
|
41278
|
+
async close() {
|
|
41279
|
+
fail();
|
|
41280
|
+
child.stdin.end();
|
|
41281
|
+
const force = setTimeout(() => child.kill("SIGKILL"), 1000);
|
|
41282
|
+
await exited;
|
|
41283
|
+
clearTimeout(force);
|
|
41284
|
+
clearTimeout(deadline);
|
|
41285
|
+
need5(JSON.stringify(executable(command)) === JSON.stringify(before), "guard_changed");
|
|
41286
|
+
}
|
|
41287
|
+
};
|
|
41288
|
+
try {
|
|
41289
|
+
await request("initialize", { clientInfo: { name: "hooks-safety-check", version: "1" }, capabilities: { experimentalApi: true } });
|
|
41290
|
+
child.stdin.write(`{"method":"initialized"}
|
|
41291
|
+
`);
|
|
41292
|
+
return rpc;
|
|
41293
|
+
} catch {
|
|
41294
|
+
await rpc.close();
|
|
41295
|
+
throw new CodexSafetyError("native_failed");
|
|
41296
|
+
}
|
|
41297
|
+
}
|
|
41298
|
+
function supportedDiscoveryWarnings(hooks, warnings) {
|
|
41299
|
+
if (warnings.length === 0)
|
|
41300
|
+
return true;
|
|
41301
|
+
if (warnings.length > 128)
|
|
41302
|
+
return false;
|
|
41303
|
+
const allowed = new Set;
|
|
41304
|
+
const sources = new Set(hooks.map((hook) => JSON.stringify([hook?.source, hook?.sourcePath])));
|
|
41305
|
+
for (const hook of hooks) {
|
|
41306
|
+
const path = hook?.sourcePath;
|
|
41307
|
+
if (typeof path !== "string" || path.length > 4096 || !isAbsolute17(path) || path !== resolve17(dirname20(path), "hooks.json") || !["user", "project"].includes(hook.source))
|
|
41308
|
+
continue;
|
|
41309
|
+
const toml = resolve17(dirname20(path), "config.toml");
|
|
41310
|
+
if (sources.has(JSON.stringify([hook.source, toml]))) {
|
|
41311
|
+
allowed.add(`loading hooks from both ${path} and ${toml}; prefer a single representation for this layer`);
|
|
41312
|
+
}
|
|
41313
|
+
}
|
|
41314
|
+
return warnings.every((warning) => typeof warning === "string" && allowed.delete(warning));
|
|
41315
|
+
}
|
|
41316
|
+
function selectCodexSafety(discovery, configuration, home, cwd, featureEnabled, requireReady = true) {
|
|
41317
|
+
const data = discovery?.data;
|
|
41318
|
+
need5(Array.isArray(data) && data.length === 1 && data[0]?.cwd === cwd && Array.isArray(data[0]?.hooks) && data[0].hooks.length <= 1e4 && Array.isArray(data[0].errors) && data[0].errors.length === 0 && Array.isArray(data[0].warnings) && supportedDiscoveryWarnings(data[0].hooks, data[0].warnings), "native_discovery_failed");
|
|
41319
|
+
const config2 = configuration?.config;
|
|
41320
|
+
need5(config2 && typeof config2 === "object" && !Array.isArray(config2), "native_discovery_failed");
|
|
41321
|
+
need5((config2.features?.hooks ?? featureEnabled) === true, "hooks_disabled");
|
|
41322
|
+
for (const hook2 of data[0].hooks)
|
|
41323
|
+
if (typeof hook2?.command === "string") {
|
|
41324
|
+
if (/\bhasna-native-safety-v\d+\b/.test(hook2.command))
|
|
41325
|
+
need5(nativeSafetyCommandBinding(hook2.command), "guard_definition_invalid");
|
|
41326
|
+
need5(!/^(?:hooks run trash-guard|hook-trash-guard)(?:\s|$)/.test(hook2.command.trim()), "guard_definition_invalid");
|
|
41327
|
+
}
|
|
41328
|
+
const selected = data[0].hooks.filter((hook2) => typeof hook2?.command === "string" && nativeSafetyCommandBinding(hook2.command)?.name === "trash-guard");
|
|
41329
|
+
need5(selected.length !== 0, "guard_missing");
|
|
41330
|
+
need5(selected.length === 1, "guard_ambiguous");
|
|
41331
|
+
const hook = selected[0], binding = nativeSafetyCommandBinding(hook.command);
|
|
41332
|
+
need5(binding.home === home && hook.eventName === "preToolUse" && hook.handlerType === "command" && hook.matcher === matcher && hook.timeoutSec === 10 && hook.async === false && hook.statusMessage === null && hook.additionalContextLimit === null && typeof hook.key === "string" && hook.key.length <= 4096 && typeof hook.sourcePath === "string" && isAbsolute17(hook.sourcePath) && hook.pluginId === null && hook.isManaged === false && ["user", "project"].includes(hook.source) && typeof hook.currentHash === "string" && /^sha256:[a-f0-9]{64}$/.test(hook.currentHash), "guard_definition_invalid");
|
|
41333
|
+
need5(typeof hook.enabled === "boolean" && ["trusted", "untrusted", "modified"].includes(hook.trustStatus), "guard_definition_invalid");
|
|
41334
|
+
if (requireReady) {
|
|
41335
|
+
need5(hook.enabled === true, "guard_disabled");
|
|
41336
|
+
need5(hook.trustStatus === "trusted", "guard_untrusted");
|
|
41337
|
+
}
|
|
41338
|
+
return { key: hook.key, command: hook.command, currentHash: hook.currentHash, sourcePath: hook.sourcePath, enabled: hook.enabled, trustStatus: hook.trustStatus };
|
|
41339
|
+
}
|
|
41340
|
+
async function readCodexSafety(rpc, home, cwd, options = {}) {
|
|
41341
|
+
need5(versions.has(rpc.version), "native_unsupported");
|
|
41342
|
+
const hooks = await rpc.request("hooks/list", { cwds: [cwd] });
|
|
41343
|
+
const configuration = await rpc.request("config/read", { cwd, includeLayers: options.includeLayers ?? false });
|
|
41344
|
+
let enabled;
|
|
41345
|
+
if (configuration?.config?.features?.hooks === undefined) {
|
|
41346
|
+
let cursor = null, count = 0;
|
|
41347
|
+
const seen = new Set;
|
|
41348
|
+
do {
|
|
41349
|
+
const page3 = await rpc.request("experimentalFeature/list", { cursor, limit: 100 });
|
|
41350
|
+
need5(Array.isArray(page3?.data) && page3.data.length <= 100 && (page3.nextCursor === null || typeof page3.nextCursor === "string"), "native_discovery_failed");
|
|
41351
|
+
count += page3.data.length;
|
|
41352
|
+
need5(count <= 1000, "native_discovery_failed");
|
|
41353
|
+
for (const feature of page3.data)
|
|
41354
|
+
if (feature?.name === "hooks") {
|
|
41355
|
+
need5(enabled === undefined && typeof feature.enabled === "boolean", "native_discovery_failed");
|
|
41356
|
+
enabled = feature.enabled;
|
|
41357
|
+
}
|
|
41358
|
+
cursor = page3.nextCursor;
|
|
41359
|
+
if (cursor !== null) {
|
|
41360
|
+
need5(cursor.length > 0 && cursor.length <= 4096 && !seen.has(cursor) && page3.data.length > 0, "native_discovery_failed");
|
|
41361
|
+
seen.add(cursor);
|
|
41362
|
+
}
|
|
41363
|
+
} while (cursor !== null);
|
|
41364
|
+
}
|
|
41365
|
+
return { guard: selectCodexSafety(hooks, configuration, home, cwd, enabled, options.requireReady ?? true), discovery: hooks, configuration };
|
|
41366
|
+
}
|
|
41367
|
+
|
|
41368
|
+
// src/lib/codex-safety-trust.ts
|
|
41369
|
+
class CodexTrustError extends Error {
|
|
41370
|
+
code;
|
|
41371
|
+
operationId;
|
|
41372
|
+
constructor(code2, operationId) {
|
|
41373
|
+
super("Codex safety trust failed (" + code2 + ").");
|
|
41374
|
+
this.code = code2;
|
|
41375
|
+
this.operationId = operationId;
|
|
41376
|
+
this.name = "CodexTrustError";
|
|
41377
|
+
}
|
|
41378
|
+
}
|
|
41379
|
+
function need7(value, code2 = "native_changed") {
|
|
41380
|
+
if (!value)
|
|
41381
|
+
throw new CodexTrustError(code2);
|
|
41382
|
+
}
|
|
41383
|
+
var sha4 = (value) => createHash12("sha256").update(value).digest("hex");
|
|
41384
|
+
function canonical4(value) {
|
|
41385
|
+
if (Array.isArray(value))
|
|
41386
|
+
return "[" + value.map(canonical4).join(",") + "]";
|
|
41387
|
+
if (value !== null && typeof value === "object")
|
|
41388
|
+
return "{" + Object.keys(value).sort().map((key) => JSON.stringify(key) + ":" + canonical4(value[key])).join(",") + "}";
|
|
41389
|
+
return JSON.stringify(value);
|
|
41390
|
+
}
|
|
41391
|
+
function mergedConfiguration(value) {
|
|
41392
|
+
const copy = structuredClone(value);
|
|
41393
|
+
if (copy?.hooks && typeof copy.hooks === "object" && !Array.isArray(copy.hooks)) {
|
|
41394
|
+
for (const event of [
|
|
41395
|
+
"PreToolUse",
|
|
41396
|
+
"PermissionRequest",
|
|
41397
|
+
"PostToolUse",
|
|
41398
|
+
"PreCompact",
|
|
41399
|
+
"PostCompact",
|
|
41400
|
+
"SessionStart",
|
|
41401
|
+
"SessionEnd",
|
|
41402
|
+
"UserPromptSubmit",
|
|
41403
|
+
"SubagentStart",
|
|
41404
|
+
"SubagentStop",
|
|
41405
|
+
"Stop",
|
|
41406
|
+
"Interrupt"
|
|
41407
|
+
]) {
|
|
41408
|
+
if (!Object.hasOwn(copy.hooks, event))
|
|
41409
|
+
copy.hooks[event] = [];
|
|
41410
|
+
}
|
|
41411
|
+
}
|
|
41412
|
+
return canonical4(copy);
|
|
41413
|
+
}
|
|
41414
|
+
function safeDirectory2(path) {
|
|
41415
|
+
for (let p = path;; p = dirname21(p)) {
|
|
41416
|
+
const s = lstatSync15(p);
|
|
41417
|
+
need7(s.isDirectory() && [0, process.getuid?.()].includes(s.uid) && !(s.mode & 18), "preimage_unsafe");
|
|
41418
|
+
if (dirname21(p) === p)
|
|
41419
|
+
return;
|
|
41420
|
+
}
|
|
41421
|
+
}
|
|
41422
|
+
function readPrivate(path) {
|
|
41423
|
+
safeDirectory2(dirname21(path));
|
|
41424
|
+
const fd = openSync7(path, constants12.O_RDONLY | constants12.O_NOFOLLOW | constants12.O_NONBLOCK);
|
|
41425
|
+
try {
|
|
41426
|
+
const s = fstatSync6(fd);
|
|
41427
|
+
need7(s.isFile() && s.uid === process.getuid?.() && !(s.mode & 63) && s.size <= 4194304, "preimage_unsafe");
|
|
41428
|
+
const bytes = readFileSync21(fd);
|
|
41429
|
+
need7(bytes.length <= 4194304, "preimage_unsafe");
|
|
41430
|
+
return bytes;
|
|
41431
|
+
} finally {
|
|
41432
|
+
closeSync7(fd);
|
|
41433
|
+
}
|
|
41434
|
+
}
|
|
41435
|
+
function syncDirectory(path) {
|
|
41436
|
+
const fd = openSync7(path, constants12.O_RDONLY | constants12.O_DIRECTORY | constants12.O_NOFOLLOW);
|
|
41437
|
+
try {
|
|
41438
|
+
fsyncSync5(fd);
|
|
41439
|
+
} finally {
|
|
41440
|
+
closeSync7(fd);
|
|
41441
|
+
}
|
|
41442
|
+
}
|
|
41443
|
+
function exclusive(path, bytes) {
|
|
41444
|
+
const fd = openSync7(path, constants12.O_CREAT | constants12.O_EXCL | constants12.O_WRONLY | constants12.O_NOFOLLOW, 384);
|
|
41445
|
+
try {
|
|
41446
|
+
writeFileSync11(fd, bytes);
|
|
41447
|
+
fsyncSync5(fd);
|
|
41448
|
+
} finally {
|
|
41449
|
+
closeSync7(fd);
|
|
41450
|
+
}
|
|
41451
|
+
syncDirectory(dirname21(path));
|
|
41452
|
+
}
|
|
41453
|
+
async function inspect(rpc, home, cwd) {
|
|
41454
|
+
const observed = await readCodexSafety(rpc, home, cwd, { includeLayers: true, requireReady: false });
|
|
41455
|
+
const { guard, configuration, discovery } = observed;
|
|
41456
|
+
const layers = configuration?.layers;
|
|
41457
|
+
need7(Array.isArray(layers) && layers.length <= 1000, "native_config_invalid");
|
|
41458
|
+
const userLayers = layers.filter((layer2) => layer2?.name?.type === "user" && layer2.name.profile === null);
|
|
41459
|
+
need7(userLayers.length === 1, "native_config_invalid");
|
|
41460
|
+
const layer = userLayers[0], path = layer.name.file;
|
|
41461
|
+
need7(typeof path === "string" && isAbsolute18(path) && typeof layer.version === "string" && /^sha256:[a-f0-9]{64}$/.test(layer.version), "native_config_invalid");
|
|
41462
|
+
const canonicalPath = realpathSync10(path), inside = relative2(home, canonicalPath);
|
|
41463
|
+
need7(inside !== "" && !inside.startsWith("..") && !isAbsolute18(inside), "native_config_invalid");
|
|
41464
|
+
const definition = discovery.data[0].hooks.find((h) => h.key === guard.key);
|
|
41465
|
+
need7(definition.source === "user", "native_config_invalid");
|
|
41466
|
+
const before = readPrivate(canonicalPath);
|
|
41467
|
+
const rawState = layer.config?.hooks?.state?.[guard.key];
|
|
41468
|
+
need7(rawState === undefined || rawState && typeof rawState === "object" && !Array.isArray(rawState), "native_config_invalid");
|
|
41469
|
+
const desired = { ...rawState, enabled: true, trusted_hash: guard.currentHash };
|
|
41470
|
+
const planBase = {
|
|
41471
|
+
schema: "hasna.hooks.codex-safety-trust/v1",
|
|
41472
|
+
nativeVersion: rpc.version,
|
|
41473
|
+
cwd,
|
|
41474
|
+
key: guard.key,
|
|
41475
|
+
currentHash: guard.currentHash,
|
|
41476
|
+
configPath: path,
|
|
41477
|
+
configVersion: layer.version,
|
|
41478
|
+
action: guard.enabled && guard.trustStatus === "trusted" ? "unchanged" : "trust_and_enable",
|
|
41479
|
+
guardVerified: true,
|
|
41480
|
+
nativeAdoptionVerified: false
|
|
41481
|
+
};
|
|
41482
|
+
const plan = { ...planBase, planDigest: sha4(canonical4({ planBase, configuration, discovery, raw: sha4(before) })) };
|
|
41483
|
+
return { ...observed, layer, layers, canonicalPath, before, desired, plan };
|
|
41484
|
+
}
|
|
41485
|
+
async function context2(options, dependencies) {
|
|
41486
|
+
const home = realpathSync10(options.home ?? homedir7()), cwd = realpathSync10(options.cwd ?? process.cwd());
|
|
41487
|
+
need7(isAbsolute18(home) && isAbsolute18(cwd), "input_invalid");
|
|
41488
|
+
return { home, cwd, rpc: await (dependencies.connect ?? connectCodexSafety)({ ...options, home }) };
|
|
41489
|
+
}
|
|
41490
|
+
async function planCodexSafetyTrust(options = {}, dependencies = {}) {
|
|
41491
|
+
const { home, cwd, rpc } = await context2(options, dependencies);
|
|
41492
|
+
try {
|
|
41493
|
+
const before = await inspect(rpc, home, cwd);
|
|
41494
|
+
await (dependencies.verify ?? verifyNativeSafetyCommand)(before.guard.command, home);
|
|
41495
|
+
need7((await inspect(rpc, home, cwd)).plan.planDigest === before.plan.planDigest);
|
|
41496
|
+
return before.plan;
|
|
41497
|
+
} finally {
|
|
41498
|
+
await rpc.close();
|
|
41499
|
+
}
|
|
41500
|
+
}
|
|
41501
|
+
async function applyCodexSafetyTrust(options, dependencies = {}) {
|
|
41502
|
+
need7(/^[a-f0-9]{64}$/.test(options.expectedPlanDigest), "input_invalid");
|
|
41503
|
+
const selected = { ...options }, { home, cwd, rpc } = await context2(selected, dependencies);
|
|
41504
|
+
let operationId;
|
|
41505
|
+
try {
|
|
41506
|
+
const before = await inspect(rpc, home, cwd);
|
|
41507
|
+
need7(before.plan.planDigest === selected.expectedPlanDigest);
|
|
41508
|
+
await (dependencies.verify ?? verifyNativeSafetyCommand)(before.guard.command, home);
|
|
41509
|
+
need7((await inspect(rpc, home, cwd)).plan.planDigest === before.plan.planDigest);
|
|
41510
|
+
if (before.plan.action === "unchanged")
|
|
41511
|
+
return { ok: true, changed: false, ...before.plan };
|
|
41512
|
+
const expectedConfiguration = structuredClone(before.configuration);
|
|
41513
|
+
const expectedLayer = expectedConfiguration.layers.find((layer) => layer.name?.type === "user" && layer.name.profile === null);
|
|
41514
|
+
for (const config2 of [expectedConfiguration.config, expectedLayer.config]) {
|
|
41515
|
+
config2.hooks ??= {};
|
|
41516
|
+
config2.hooks.state ??= {};
|
|
41517
|
+
config2.hooks.state[before.guard.key] = before.desired;
|
|
41518
|
+
}
|
|
41519
|
+
const expectedDiscovery = structuredClone(before.discovery);
|
|
41520
|
+
const expectedGuard = expectedDiscovery.data[0].hooks.find((h) => h.key === before.guard.key);
|
|
41521
|
+
expectedGuard.enabled = true;
|
|
41522
|
+
expectedGuard.trustStatus = "trusted";
|
|
41523
|
+
operationId = randomUUID3();
|
|
41524
|
+
let directory2 = home;
|
|
41525
|
+
for (const part of [".hasna", "hooks", "native", "codex-trash-trust", operationId]) {
|
|
41526
|
+
directory2 = join24(directory2, part);
|
|
41527
|
+
try {
|
|
41528
|
+
mkdirSync13(directory2, { mode: 448 });
|
|
41529
|
+
syncDirectory(dirname21(directory2));
|
|
41530
|
+
} catch (error61) {
|
|
41531
|
+
if (error61.code !== "EEXIST")
|
|
41532
|
+
throw error61;
|
|
41533
|
+
}
|
|
41534
|
+
safeDirectory2(directory2);
|
|
41535
|
+
}
|
|
41536
|
+
exclusive(join24(directory2, "config.before"), before.before);
|
|
41537
|
+
exclusive(join24(directory2, "intent.json"), JSON.stringify({ operationId, plan: before.plan, configSHA256: sha4(before.before) }) + `
|
|
41538
|
+
`);
|
|
41539
|
+
need7(sha4(readPrivate(before.canonicalPath)) === sha4(before.before));
|
|
41540
|
+
const result = await rpc.request("config/batchWrite", {
|
|
41541
|
+
filePath: before.plan.configPath,
|
|
41542
|
+
expectedVersion: before.plan.configVersion,
|
|
41543
|
+
reloadUserConfig: true,
|
|
41544
|
+
edits: [{ keyPath: "hooks.state." + JSON.stringify(before.guard.key), value: before.desired, mergeStrategy: "replace" }]
|
|
41545
|
+
});
|
|
41546
|
+
need7(result?.status === "ok" && result.filePath === before.plan.configPath && result.overriddenMetadata === null && /^sha256:[a-f0-9]{64}$/.test(result.version));
|
|
41547
|
+
const after = await inspect(rpc, home, cwd);
|
|
41548
|
+
expectedLayer.version = result.version;
|
|
41549
|
+
need7(after.layer.version === result.version && after.plan.action === "unchanged" && mergedConfiguration(after.configuration.config) === mergedConfiguration(expectedConfiguration.config) && canonical4(after.configuration.layers) === canonical4(expectedConfiguration.layers) && canonical4(after.discovery) === canonical4(expectedDiscovery));
|
|
41550
|
+
const receipt = {
|
|
41551
|
+
ok: true,
|
|
41552
|
+
changed: true,
|
|
41553
|
+
operationId,
|
|
41554
|
+
planDigest: before.plan.planDigest,
|
|
41555
|
+
configVersion: result.version,
|
|
41556
|
+
guardVerified: true,
|
|
41557
|
+
nativeDiscoveryVerified: true,
|
|
41558
|
+
nativeAdoptionVerified: false,
|
|
41559
|
+
unrelatedConfigurationPreserved: true
|
|
41560
|
+
};
|
|
41561
|
+
exclusive(join24(directory2, "receipt.json"), JSON.stringify(receipt) + `
|
|
41562
|
+
`);
|
|
41563
|
+
return receipt;
|
|
41564
|
+
} catch (error61) {
|
|
41565
|
+
if (operationId)
|
|
41566
|
+
throw new CodexTrustError("trust_outcome_uncertain", operationId);
|
|
41567
|
+
throw error61;
|
|
41568
|
+
} finally {
|
|
41569
|
+
await rpc.close();
|
|
41570
|
+
}
|
|
41571
|
+
}
|
|
41572
|
+
// src/lib/claude-safety-check.ts
|
|
41573
|
+
import { constants as constants14, closeSync as closeSync15, fstatSync as fstatSync7, lstatSync as lstatSync21, openSync as openSync15, readFileSync as readFileSync23, realpathSync as realpathSync11 } from "fs";
|
|
41574
|
+
import { createHash as createHash17 } from "crypto";
|
|
41575
|
+
import { dirname as dirname22, isAbsolute as isAbsolute19, join as join27, resolve as resolve18 } from "path";
|
|
41576
|
+
import { homedir as homedir8 } from "os";
|
|
41577
|
+
class ClaudeSafetyError extends Error {
|
|
41578
|
+
code;
|
|
41579
|
+
constructor(code2) {
|
|
41580
|
+
super("Claude safety configuration check failed (" + code2 + ").");
|
|
41581
|
+
this.code = code2;
|
|
41582
|
+
this.name = "ClaudeSafetyError";
|
|
41583
|
+
}
|
|
41584
|
+
}
|
|
41585
|
+
function need8(value, code2) {
|
|
41586
|
+
if (!value)
|
|
41587
|
+
throw new ClaudeSafetyError(code2);
|
|
41588
|
+
}
|
|
41589
|
+
var sha6 = (value) => createHash17("sha256").update(value).digest("hex");
|
|
41590
|
+
function read3(path) {
|
|
41591
|
+
try {
|
|
41592
|
+
const resolved = realpathSync11(path);
|
|
41593
|
+
for (let parent = dirname22(resolved);; parent = dirname22(parent)) {
|
|
41594
|
+
const s = lstatSync21(parent);
|
|
41595
|
+
need8(s.isDirectory() && [0, process.getuid?.()].includes(s.uid) && !(s.mode & 18), "settings_unsafe");
|
|
41596
|
+
if (dirname22(parent) === parent)
|
|
41597
|
+
break;
|
|
41598
|
+
}
|
|
41599
|
+
const fd = openSync15(resolved, constants14.O_RDONLY | constants14.O_NOFOLLOW | constants14.O_NONBLOCK);
|
|
41600
|
+
try {
|
|
41601
|
+
const s = fstatSync7(fd);
|
|
41602
|
+
need8(s.isFile() && [0, process.getuid?.()].includes(s.uid) && !(s.mode & 18) && s.size <= 1048576, "settings_unsafe");
|
|
41603
|
+
const raw = readFileSync23(fd, "utf8");
|
|
41604
|
+
need8(Buffer.byteLength(raw) <= 1048576, "settings_unsafe");
|
|
41605
|
+
return { path, resolved, sha256: sha6(raw), raw };
|
|
41606
|
+
} finally {
|
|
41607
|
+
closeSync15(fd);
|
|
41608
|
+
}
|
|
41609
|
+
} catch (error61) {
|
|
41610
|
+
if (error61.code === "ENOENT")
|
|
41611
|
+
return { path, resolved: null, sha256: null, raw: null };
|
|
41612
|
+
if (error61 instanceof ClaudeSafetyError)
|
|
41613
|
+
throw error61;
|
|
41614
|
+
throw new ClaudeSafetyError("settings_unsafe");
|
|
41615
|
+
}
|
|
41616
|
+
}
|
|
41617
|
+
async function verifyClaudeSafetyConfiguration(options = {}, dependencies = {}) {
|
|
41618
|
+
const home = realpathSync11(options.home ?? homedir8()), cwd = realpathSync11(options.cwd ?? process.cwd());
|
|
41619
|
+
const paths = [
|
|
41620
|
+
options.userSettingsPath ?? join27(home, ".claude/settings.json"),
|
|
41621
|
+
join27(cwd, ".claude/settings.json"),
|
|
41622
|
+
options.localSettingsPath ?? join27(cwd, ".claude/settings.local.json")
|
|
41623
|
+
];
|
|
41624
|
+
need8(paths.every((path) => isAbsolute19(path) && resolve18(path) === path), "settings_unsafe");
|
|
41625
|
+
const inspect2 = () => [...new Set(paths)].map(read3);
|
|
41626
|
+
const before = inspect2(), unique = [...new Map(before.filter((file2) => file2.resolved !== null).map((file2) => [file2.resolved, file2])).values()];
|
|
41627
|
+
const guards = [];
|
|
41628
|
+
for (const file2 of unique) {
|
|
41629
|
+
let settings;
|
|
41630
|
+
try {
|
|
41631
|
+
settings = JSON.parse(file2.raw);
|
|
41632
|
+
} catch {
|
|
41633
|
+
throw new ClaudeSafetyError("settings_invalid");
|
|
41634
|
+
}
|
|
41635
|
+
need8(settings && typeof settings === "object" && !Array.isArray(settings), "settings_invalid");
|
|
41636
|
+
need8(settings.disableAllHooks === undefined || settings.disableAllHooks === false, "hooks_disabled");
|
|
41637
|
+
if (settings.hooks === undefined)
|
|
41638
|
+
continue;
|
|
41639
|
+
need8(settings.hooks && typeof settings.hooks === "object" && !Array.isArray(settings.hooks), "settings_invalid");
|
|
41640
|
+
let count = 0;
|
|
41641
|
+
for (const [event, groups] of Object.entries(settings.hooks)) {
|
|
41642
|
+
need8(Array.isArray(groups), "settings_invalid");
|
|
41643
|
+
for (const group of groups) {
|
|
41644
|
+
need8(group && Array.isArray(group.hooks), "settings_invalid");
|
|
41645
|
+
for (const handler of group.hooks) {
|
|
41646
|
+
need8(++count <= 1e4 && handler && typeof handler === "object", "settings_invalid");
|
|
41647
|
+
if (typeof handler.command !== "string")
|
|
41648
|
+
continue;
|
|
41649
|
+
const binding = nativeSafetyCommandBinding(handler.command);
|
|
41650
|
+
if (/\bhasna-native-safety-v\d+\b/.test(handler.command))
|
|
41651
|
+
need8(binding, "guard_definition_invalid");
|
|
41652
|
+
need8(!/^(?:hooks run trash-guard|hook-trash-guard)(?:\s|$)/.test(handler.command.trim()), "guard_definition_invalid");
|
|
41653
|
+
if (binding?.name !== "trash-guard")
|
|
41654
|
+
continue;
|
|
41655
|
+
need8(binding.home === home && event === "PreToolUse" && handler.type === "command" && group.matcher === "^(Bash|apply_patch|ApplyPatch|functions\\.apply_patch)$" && handler.timeout === 10 && (handler.async === undefined || handler.async === false) && handler.asyncRewake !== true, "guard_definition_invalid");
|
|
41656
|
+
guards.push({ command: handler.command, path: file2.path });
|
|
41657
|
+
}
|
|
41658
|
+
}
|
|
41659
|
+
}
|
|
41660
|
+
}
|
|
41661
|
+
need8(guards.length !== 0, "guard_missing");
|
|
41662
|
+
need8(guards.length === 1, "guard_ambiguous");
|
|
41663
|
+
try {
|
|
41664
|
+
await (dependencies.verify ?? verifyNativeSafetyCommand)(guards[0].command, home);
|
|
41665
|
+
} catch {
|
|
41666
|
+
throw new ClaudeSafetyError("guard_failed");
|
|
41667
|
+
}
|
|
41668
|
+
need8(JSON.stringify(inspect2()) === JSON.stringify(before), "settings_changed");
|
|
41669
|
+
return {
|
|
41670
|
+
ok: true,
|
|
41671
|
+
target: "claude",
|
|
41672
|
+
cwd,
|
|
41673
|
+
configurationVerified: true,
|
|
41674
|
+
guardVerified: true,
|
|
41675
|
+
sources: before.map(({ path, sha256 }) => ({ path, sha256 })),
|
|
41676
|
+
guardSHA256: sha6(guards[0].command),
|
|
41677
|
+
runtimePolicyVerified: false,
|
|
41678
|
+
nativeDiscoveryVerified: false,
|
|
41679
|
+
nativeAdoptionVerified: false
|
|
41680
|
+
};
|
|
41681
|
+
}
|
|
41682
|
+
// src/lib/native-safety-registration.ts
|
|
41683
|
+
import { createHash as createHash19, randomUUID as randomUUID5 } from "crypto";
|
|
41684
|
+
import { constants as constants15, closeSync as closeSync17, fstatSync as fstatSync15, fsyncSync as fsyncSync7, lstatSync as lstatSync23, mkdirSync as mkdirSync14, openSync as openSync17, readFileSync as readFileSync25, realpathSync as realpathSync12, writeFileSync as writeFileSync12 } from "fs";
|
|
41685
|
+
import { basename as basename9, dirname as dirname23, isAbsolute as isAbsolute20, join as join29, relative as relative3, resolve as resolve19 } from "path";
|
|
41686
|
+
import { homedir as homedir9 } from "os";
|
|
41687
|
+
class NativeRegistrationError extends Error {
|
|
41688
|
+
code;
|
|
41689
|
+
operationId;
|
|
41690
|
+
constructor(code2, operationId) {
|
|
41691
|
+
super("Native safety registration failed (" + code2 + ").");
|
|
41692
|
+
this.code = code2;
|
|
41693
|
+
this.operationId = operationId;
|
|
41694
|
+
this.name = "NativeRegistrationError";
|
|
41695
|
+
}
|
|
41696
|
+
}
|
|
41697
|
+
var need9 = (value, code2) => {
|
|
41698
|
+
if (!value)
|
|
41699
|
+
throw new NativeRegistrationError(code2);
|
|
41700
|
+
};
|
|
41701
|
+
var sha7 = (value) => createHash19("sha256").update(value).digest("hex");
|
|
41702
|
+
function canonical6(value) {
|
|
41703
|
+
if (Array.isArray(value))
|
|
41704
|
+
return "[" + value.map(canonical6).join(",") + "]";
|
|
41705
|
+
if (value !== null && typeof value === "object")
|
|
41706
|
+
return "{" + Object.keys(value).sort().map((key) => JSON.stringify(key) + ":" + canonical6(value[key])).join(",") + "}";
|
|
41707
|
+
return JSON.stringify(value);
|
|
41708
|
+
}
|
|
41709
|
+
function safeDirectory3(path) {
|
|
41710
|
+
for (let p = path;; p = dirname23(p)) {
|
|
41711
|
+
const s = lstatSync23(p);
|
|
41712
|
+
need9(s.isDirectory() && [0, process.getuid?.()].includes(s.uid) && !(s.mode & 18), "settings_unsafe");
|
|
41713
|
+
if (dirname23(p) === p)
|
|
41714
|
+
return;
|
|
41715
|
+
}
|
|
41716
|
+
}
|
|
41717
|
+
function read5(path) {
|
|
41718
|
+
let resolved;
|
|
41719
|
+
try {
|
|
41720
|
+
resolved = realpathSync12(path);
|
|
41721
|
+
} catch (error61) {
|
|
41722
|
+
if (error61.code !== "ENOENT")
|
|
41723
|
+
throw new NativeRegistrationError("settings_unsafe");
|
|
41724
|
+
try {
|
|
41725
|
+
lstatSync23(path);
|
|
41726
|
+
throw new NativeRegistrationError("settings_unsafe");
|
|
41727
|
+
} catch (missing2) {
|
|
41728
|
+
if (missing2.code !== "ENOENT")
|
|
41729
|
+
throw missing2;
|
|
41730
|
+
}
|
|
41731
|
+
let ancestor = dirname23(path);
|
|
41732
|
+
const suffix = [basename9(path)];
|
|
41733
|
+
for (;; ) {
|
|
41734
|
+
try {
|
|
41735
|
+
resolved = join29(realpathSync12(ancestor), ...suffix);
|
|
41736
|
+
break;
|
|
41737
|
+
} catch (missing2) {
|
|
41738
|
+
if (missing2.code !== "ENOENT" || dirname23(ancestor) === ancestor || suffix.length >= 64)
|
|
41739
|
+
throw new NativeRegistrationError("settings_unsafe");
|
|
41740
|
+
try {
|
|
41741
|
+
lstatSync23(ancestor);
|
|
41742
|
+
throw new NativeRegistrationError("settings_unsafe");
|
|
41743
|
+
} catch (absent) {
|
|
41744
|
+
if (absent.code !== "ENOENT")
|
|
41745
|
+
throw absent;
|
|
41746
|
+
}
|
|
41747
|
+
suffix.unshift(basename9(ancestor));
|
|
41748
|
+
ancestor = dirname23(ancestor);
|
|
41749
|
+
}
|
|
41750
|
+
}
|
|
41751
|
+
safeDirectory3(realpathSync12(ancestor));
|
|
41752
|
+
return { resolved, raw: null, sha256: "absent" };
|
|
41753
|
+
}
|
|
41754
|
+
safeDirectory3(dirname23(resolved));
|
|
41755
|
+
let fd;
|
|
41756
|
+
try {
|
|
41757
|
+
fd = openSync17(resolved, constants15.O_RDONLY | constants15.O_NOFOLLOW | constants15.O_NONBLOCK);
|
|
41758
|
+
} catch (error61) {
|
|
41759
|
+
if (error61.code === "ENOENT")
|
|
41760
|
+
return { resolved, raw: null, sha256: "absent" };
|
|
41761
|
+
throw new NativeRegistrationError("settings_unsafe");
|
|
41762
|
+
}
|
|
41763
|
+
try {
|
|
41764
|
+
const s = fstatSync15(fd);
|
|
41765
|
+
need9(s.isFile() && s.uid === process.getuid?.() && !(s.mode & 18) && s.nlink === 1 && s.size <= 1048576, "settings_unsafe");
|
|
41766
|
+
const bytes = readFileSync25(fd);
|
|
41767
|
+
need9(bytes.length <= 1048576, "settings_unsafe");
|
|
41768
|
+
const raw = bytes.toString("utf8");
|
|
41769
|
+
need9(Buffer.from(raw).equals(bytes), "settings_invalid");
|
|
41770
|
+
return { resolved, raw, sha256: sha7(bytes) };
|
|
41771
|
+
} finally {
|
|
41772
|
+
closeSync17(fd);
|
|
41773
|
+
}
|
|
41774
|
+
}
|
|
41775
|
+
function assertLosslessJson(raw) {
|
|
41776
|
+
const tokens = /"(?:\\[\s\S]|[^"\\])*"|-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?|true|false|null|[{}\[\],:]/g;
|
|
41777
|
+
const stack = [];
|
|
41778
|
+
function decimal(text3) {
|
|
41779
|
+
const match = /^(-?)([0-9]+)(?:\.([0-9]+))?(?:[eE]([+-]?[0-9]+))?$/.exec(text3);
|
|
41780
|
+
need9((match[4]?.replace(/^[+-]/, "").length ?? 0) <= 6, "settings_invalid");
|
|
41781
|
+
let digits = (match[2] + (match[3] ?? "")).replace(/^0+/, "");
|
|
41782
|
+
if (!digits)
|
|
41783
|
+
return match[1] ? "-0" : "0";
|
|
41784
|
+
let exponent = Number(match[4] ?? "0") - (match[3]?.length ?? 0);
|
|
41785
|
+
while (digits.endsWith("0")) {
|
|
41786
|
+
digits = digits.slice(0, -1);
|
|
41787
|
+
exponent++;
|
|
41788
|
+
}
|
|
41789
|
+
return match[1] + digits + "e" + exponent;
|
|
41790
|
+
}
|
|
41791
|
+
for (const match of raw.matchAll(tokens)) {
|
|
41792
|
+
const token = match[0], current2 = stack.at(-1);
|
|
41793
|
+
if (token === "{")
|
|
41794
|
+
stack.push({ kind: "object", key: true, names: new Set });
|
|
41795
|
+
else if (token === "[")
|
|
41796
|
+
stack.push({ kind: "array", key: false, names: new Set });
|
|
41797
|
+
else if (token === "}" || token === "]")
|
|
41798
|
+
stack.pop();
|
|
41799
|
+
else if (token === "," && current2?.kind === "object")
|
|
41800
|
+
current2.key = true;
|
|
41801
|
+
else if (token.startsWith('"') && current2?.kind === "object" && current2.key) {
|
|
41802
|
+
const key = JSON.parse(token);
|
|
41803
|
+
need9(!current2.names.has(key), "settings_invalid");
|
|
41804
|
+
current2.names.add(key);
|
|
41805
|
+
current2.key = false;
|
|
41806
|
+
} else if (/^-?[0-9]/.test(token)) {
|
|
41807
|
+
const parsed = Number(token);
|
|
41808
|
+
need9(Number.isFinite(parsed) && decimal(token) === decimal(JSON.stringify(parsed)), "settings_invalid");
|
|
41809
|
+
}
|
|
41810
|
+
}
|
|
41811
|
+
}
|
|
41812
|
+
function validateSettings(raw) {
|
|
41813
|
+
let settings;
|
|
41814
|
+
try {
|
|
41815
|
+
settings = JSON.parse(raw ?? "{}");
|
|
41816
|
+
} catch {
|
|
41817
|
+
throw new NativeRegistrationError("settings_invalid");
|
|
41818
|
+
}
|
|
41819
|
+
need9(settings && typeof settings === "object" && !Array.isArray(settings), "settings_invalid");
|
|
41820
|
+
assertLosslessJson(raw ?? "{}");
|
|
41821
|
+
need9(settings.disableAllHooks === undefined || settings.disableAllHooks === false, "hooks_disabled");
|
|
41822
|
+
if (settings.hooks !== undefined) {
|
|
41823
|
+
need9(settings.hooks && typeof settings.hooks === "object" && !Array.isArray(settings.hooks), "settings_invalid");
|
|
41824
|
+
for (const groups of Object.values(settings.hooks)) {
|
|
41825
|
+
need9(Array.isArray(groups), "settings_invalid");
|
|
41826
|
+
for (const group of groups) {
|
|
41827
|
+
need9(group && Array.isArray(group.hooks), "settings_invalid");
|
|
41828
|
+
for (const hook of group.hooks) {
|
|
41829
|
+
need9(hook && typeof hook === "object", "settings_invalid");
|
|
41830
|
+
if (registrationName(hook.command) === "trash-guard") {
|
|
41831
|
+
need9((hook.async === undefined || hook.async === false) && hook.asyncRewake !== true, "registration_conflict");
|
|
41832
|
+
}
|
|
41833
|
+
}
|
|
41834
|
+
}
|
|
41835
|
+
}
|
|
41836
|
+
}
|
|
41837
|
+
return settings;
|
|
41838
|
+
}
|
|
41839
|
+
function inspect2(options) {
|
|
41840
|
+
need9(options?.target === "codex" || options?.target === "claude", "input_invalid");
|
|
41841
|
+
const home = realpathSync12(homedir9()), path = getSettingsPath("global", options.target);
|
|
41842
|
+
need9(isAbsolute20(path) && resolve19(path) === path, "input_invalid");
|
|
41843
|
+
const nativeHome = options.target === "codex" ? process.env.CODEX_HOME : process.env.CLAUDE_CONFIG_DIR;
|
|
41844
|
+
if (nativeHome !== undefined) {
|
|
41845
|
+
need9(nativeHome.length > 0 && isAbsolute20(nativeHome), "input_invalid");
|
|
41846
|
+
const selectedNative = read5(resolve19(nativeHome, options.target === "codex" ? "hooks.json" : "settings.json"));
|
|
41847
|
+
need9(read5(path).resolved === selectedNative.resolved, "input_invalid");
|
|
41848
|
+
}
|
|
41849
|
+
const before = read5(path), inside = relative3(home, before.resolved);
|
|
41850
|
+
need9(inside !== "" && !inside.startsWith("..") && !isAbsolute20(inside), "settings_unsafe");
|
|
41851
|
+
const parsed = validateSettings(before.raw);
|
|
41852
|
+
let preview2;
|
|
41853
|
+
try {
|
|
41854
|
+
preview2 = previewNativeSafetyRegistration(options.target);
|
|
41855
|
+
} catch {
|
|
41856
|
+
throw new NativeRegistrationError("registration_conflict");
|
|
41857
|
+
}
|
|
41858
|
+
need9(preview2.before === before.raw, "settings_changed");
|
|
41859
|
+
validateSettings(preview2.after);
|
|
41860
|
+
const changed = canonical6(parsed) !== canonical6(JSON.parse(preview2.after));
|
|
41861
|
+
const base = {
|
|
41862
|
+
schema: "hasna.hooks.native-safety-registration/v1",
|
|
41863
|
+
target: options.target,
|
|
41864
|
+
settingsPath: path,
|
|
41865
|
+
resolvedPath: before.resolved,
|
|
41866
|
+
beforeSHA256: before.sha256,
|
|
41867
|
+
desiredSHA256: changed ? sha7(preview2.after) : before.sha256,
|
|
41868
|
+
commandSHA256: sha7(preview2.command),
|
|
41869
|
+
action: changed ? "register" : "unchanged",
|
|
41870
|
+
guardVerified: true,
|
|
41871
|
+
nativeAdoptionVerified: false
|
|
41872
|
+
};
|
|
41873
|
+
const plan = { ...base, planDigest: sha7(canonical6(base)) };
|
|
41874
|
+
need9(canonical6(read5(path)) === canonical6(before), "settings_changed");
|
|
41875
|
+
return { home, before, preview: preview2, plan };
|
|
41876
|
+
}
|
|
41877
|
+
function syncDirectory2(path) {
|
|
41878
|
+
const fd = openSync17(path, constants15.O_RDONLY | constants15.O_DIRECTORY | constants15.O_NOFOLLOW);
|
|
41879
|
+
try {
|
|
41880
|
+
fsyncSync7(fd);
|
|
41881
|
+
} finally {
|
|
41882
|
+
closeSync17(fd);
|
|
41883
|
+
}
|
|
41884
|
+
}
|
|
41885
|
+
function exclusive2(path, bytes) {
|
|
41886
|
+
const fd = openSync17(path, constants15.O_CREAT | constants15.O_EXCL | constants15.O_WRONLY | constants15.O_NOFOLLOW, 384);
|
|
41887
|
+
try {
|
|
41888
|
+
writeFileSync12(fd, bytes);
|
|
41889
|
+
fsyncSync7(fd);
|
|
41890
|
+
} finally {
|
|
41891
|
+
closeSync17(fd);
|
|
41892
|
+
}
|
|
41893
|
+
syncDirectory2(dirname23(path));
|
|
41894
|
+
}
|
|
41895
|
+
async function planNativeSafetyRegistration(options, dependencies = {}) {
|
|
41896
|
+
const selected = { ...options }, before = inspect2(selected);
|
|
41897
|
+
try {
|
|
41898
|
+
await (dependencies.verify ?? verifyNativeSafetyCommand)(before.preview.command, before.home);
|
|
41899
|
+
} catch {
|
|
41900
|
+
throw new NativeRegistrationError("guard_failed");
|
|
41901
|
+
}
|
|
41902
|
+
need9(inspect2(selected).plan.planDigest === before.plan.planDigest, "settings_changed");
|
|
41903
|
+
return before.plan;
|
|
41904
|
+
}
|
|
41905
|
+
async function applyNativeSafetyRegistration(options, dependencies = {}) {
|
|
41906
|
+
const selected = { ...options };
|
|
41907
|
+
need9(/^[a-f0-9]{64}$/.test(selected.expectedPlanDigest), "input_invalid");
|
|
41908
|
+
const plan = await planNativeSafetyRegistration(selected, dependencies);
|
|
41909
|
+
need9(plan.planDigest === selected.expectedPlanDigest, "settings_changed");
|
|
41910
|
+
const before = inspect2(selected);
|
|
41911
|
+
need9(before.plan.planDigest === plan.planDigest, "settings_changed");
|
|
41912
|
+
if (plan.action === "unchanged")
|
|
41913
|
+
return { ok: true, changed: false, ...plan };
|
|
41914
|
+
const operationId = randomUUID5();
|
|
41915
|
+
let directory2 = before.home;
|
|
41916
|
+
for (const part of [".hasna", "hooks", "native", "trash-registration", operationId]) {
|
|
41917
|
+
directory2 = join29(directory2, part);
|
|
41918
|
+
try {
|
|
41919
|
+
mkdirSync14(directory2, { mode: 448 });
|
|
41920
|
+
syncDirectory2(dirname23(directory2));
|
|
41921
|
+
} catch (error61) {
|
|
41922
|
+
if (error61.code !== "EEXIST")
|
|
41923
|
+
throw new NativeRegistrationError("settings_unsafe");
|
|
41924
|
+
}
|
|
41925
|
+
safeDirectory3(directory2);
|
|
41926
|
+
}
|
|
41927
|
+
if (before.before.raw !== null)
|
|
41928
|
+
exclusive2(join29(directory2, "settings.before"), before.before.raw);
|
|
41929
|
+
exclusive2(join29(directory2, "intent.json"), JSON.stringify({ operationId, plan }) + `
|
|
41930
|
+
`);
|
|
41931
|
+
try {
|
|
41932
|
+
need9(inspect2(selected).plan.planDigest === plan.planDigest, "settings_changed");
|
|
41933
|
+
const result = installHook("trash-guard", { target: selected.target, overwrite: true, expectedSettingsSha256: plan.beforeSHA256 });
|
|
41934
|
+
need9(result.success, "apply_outcome_uncertain");
|
|
41935
|
+
const after = inspect2(selected);
|
|
41936
|
+
need9(after.before.sha256 === plan.desiredSHA256 && after.plan.resolvedPath === plan.resolvedPath && after.plan.action === "unchanged" && after.plan.commandSHA256 === plan.commandSHA256, "apply_outcome_uncertain");
|
|
41937
|
+
const receipt = {
|
|
41938
|
+
ok: true,
|
|
41939
|
+
changed: true,
|
|
41940
|
+
operationId,
|
|
41941
|
+
planDigest: plan.planDigest,
|
|
41942
|
+
settingsSHA256: after.before.sha256,
|
|
41943
|
+
guardVerified: true,
|
|
41944
|
+
nativeAdoptionVerified: false
|
|
41945
|
+
};
|
|
41946
|
+
exclusive2(join29(directory2, "receipt.json"), JSON.stringify(receipt) + `
|
|
41947
|
+
`);
|
|
41948
|
+
return receipt;
|
|
41949
|
+
} catch {
|
|
41950
|
+
throw new NativeRegistrationError("apply_outcome_uncertain", operationId);
|
|
41951
|
+
}
|
|
41952
|
+
}
|
|
40980
41953
|
|
|
40981
41954
|
// src/index.ts
|
|
40982
41955
|
function installHookForProject(name, options = {}) {
|
|
@@ -40999,11 +41972,11 @@ async function runHook(name, input2, options = {}) {
|
|
|
40999
41972
|
throw new Error(`Hook '${name}' not found`);
|
|
41000
41973
|
const hookScript = getHookPath(name) + "/src/hook.ts";
|
|
41001
41974
|
const resolvedScript = custom3?.scriptPath;
|
|
41002
|
-
const script = resolvedScript ?? (
|
|
41975
|
+
const script = resolvedScript ?? (existsSync15(hookScript) ? hookScript : undefined);
|
|
41003
41976
|
if (!script)
|
|
41004
41977
|
throw new Error(`Hook script not found: ${name}`);
|
|
41005
41978
|
const { sha256Of: sha256Of2, checkScriptHash: checkScriptHash2 } = await Promise.resolve().then(() => (init_store(), exports_store));
|
|
41006
|
-
const content =
|
|
41979
|
+
const content = readFileSync26(script);
|
|
41007
41980
|
const check2 = checkScriptHash2(name, sha256Of2(content));
|
|
41008
41981
|
if (!check2.ok) {
|
|
41009
41982
|
throw new Error(`Hook '${name}' script changed since it was trusted (sha256 ${check2.expected} != ${check2.actual}). Run 'hooks trust ${name}' to trust the new content.`);
|
|
@@ -41095,6 +42068,7 @@ export {
|
|
|
41095
42068
|
writeLock,
|
|
41096
42069
|
writeCustomHook,
|
|
41097
42070
|
verifyScriptHash,
|
|
42071
|
+
verifyClaudeSafetyConfiguration,
|
|
41098
42072
|
upsertHookRecord,
|
|
41099
42073
|
updateProfile,
|
|
41100
42074
|
uninstallHook,
|
|
@@ -41133,6 +42107,8 @@ export {
|
|
|
41133
42107
|
readCustomManifest,
|
|
41134
42108
|
provisionCloudflareResources,
|
|
41135
42109
|
planSync,
|
|
42110
|
+
planNativeSafetyRegistration,
|
|
42111
|
+
planCodexSafetyTrust,
|
|
41136
42112
|
pinInstalledHook,
|
|
41137
42113
|
parseStorageTables,
|
|
41138
42114
|
parseManifest,
|
|
@@ -41186,12 +42162,15 @@ export {
|
|
|
41186
42162
|
createProfile,
|
|
41187
42163
|
checkScriptHash,
|
|
41188
42164
|
buildCodewithTomlFragment,
|
|
42165
|
+
applyNativeSafetyRegistration,
|
|
42166
|
+
applyCodexSafetyTrust,
|
|
41189
42167
|
allowLocalStore,
|
|
41190
42168
|
STORAGE_TABLES,
|
|
41191
42169
|
STORAGE_DATABASE_ENV,
|
|
41192
42170
|
STORAGE_BACKEND_ENV,
|
|
41193
42171
|
STORAGE_BACKENDS,
|
|
41194
42172
|
RETIRED_STORAGE_MODE_ENV,
|
|
42173
|
+
NativeRegistrationError,
|
|
41195
42174
|
HOOK_EVENTS,
|
|
41196
42175
|
HOOKS_STORAGE_TABLES,
|
|
41197
42176
|
HOOKS_STORAGE_FALLBACK_ENV,
|
|
@@ -41201,5 +42180,7 @@ export {
|
|
|
41201
42180
|
HOOKS_LOCAL_OPT_IN_ENV_KEYS,
|
|
41202
42181
|
HOOKS,
|
|
41203
42182
|
DEFAULT_SERVE_PORT,
|
|
42183
|
+
CodexTrustError,
|
|
42184
|
+
ClaudeSafetyError,
|
|
41204
42185
|
CATEGORIES
|
|
41205
42186
|
};
|