@hasna/hooks 0.10.6 → 0.10.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +82 -1
- package/bin/hooks-mcp.js +100 -44
- package/bin/index.js +7835 -6947
- package/bin/native-safety-entry.js +196 -15
- package/dist/index.d.ts +3 -0
- package/dist/index.js +1082 -68
- package/dist/lib/claude-safety-check.d.ts +32 -0
- package/dist/lib/codex-safety-check.d.ts +58 -0
- package/dist/lib/codex-safety-trust.d.ts +54 -0
- package/dist/lib/codex-settings.d.ts +5 -1
- package/dist/lib/installer.d.ts +8 -0
- package/dist/lib/native-safety-registration.d.ts +52 -0
- package/dist/lib/native-safety.d.ts +4 -0
- package/dist/native-safety.d.ts +6 -0
- package/dist/native-safety.js +18 -0
- package/hooks/hook-workspace-repos-guard/README.md +12 -0
- package/hooks/hook-workspace-repos-guard/src/hook.ts +247 -14
- package/package.json +2 -2
- package/scripts/ensure-profiles-dir.mjs +65 -1
package/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);
|
|
@@ -7923,19 +7941,20 @@ function writeCodexSettings(path, settings) {
|
|
|
7923
7941
|
// src/lib/claude-skills-coordination.ts
|
|
7924
7942
|
import { createHash as createHash5 } from "crypto";
|
|
7925
7943
|
|
|
7926
|
-
// ../../node_modules/.bun/@hasna+skills@0.9.
|
|
7944
|
+
// ../../node_modules/.bun/@hasna+skills@0.9.17+a00d3064559565bb/node_modules/@hasna/skills/dist/index.js
|
|
7927
7945
|
import { existsSync as existsSync9 } from "fs";
|
|
7928
7946
|
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";
|
|
7936
7954
|
import { createHash as createHash16 } from "crypto";
|
|
7937
7955
|
import { lstatSync as lstatSync20, readFileSync as readFileSync16, readdirSync as readdirSync9, realpathSync as realpathSync4 } from "fs";
|
|
7938
7956
|
import { basename as basename8, dirname as dirname16, isAbsolute as isAbsolute14, join as join26, resolve as resolve14, sep as sep5 } from "path";
|
|
7957
|
+
import { isDeepStrictEqual as isDeepStrictEqual3 } from "util";
|
|
7939
7958
|
import { dirname as dirname32, isAbsolute as isAbsolute3, join as join72, resolve as resolve32 } from "path";
|
|
7940
7959
|
import { lstatSync as lstatSync3, readFileSync as readFileSync6, opendirSync } from "fs";
|
|
7941
7960
|
import { createHash as createHash22 } from "crypto";
|
|
@@ -7944,7 +7963,7 @@ import { dirname as dirname42, isAbsolute as isAbsolute4, join as join82, resolv
|
|
|
7944
7963
|
import { createHash as createHash32 } from "crypto";
|
|
7945
7964
|
import { closeSync as closeSync3, constants as constants2, fstatSync as fstatSync2, lstatSync as lstatSync5, openSync as openSync3, readSync } from "fs";
|
|
7946
7965
|
import { dirname as dirname5, isAbsolute as isAbsolute5, resolve as resolve5 } from "path";
|
|
7947
|
-
import { createHash as
|
|
7966
|
+
import { createHash as createHash42 } from "crypto";
|
|
7948
7967
|
import { lstatSync as lstatSync6, readlinkSync } from "fs";
|
|
7949
7968
|
import { dirname as dirname6, isAbsolute as isAbsolute6, join as join9, parse, resolve as resolve6, sep as sep22 } from "path";
|
|
7950
7969
|
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 +15091,7 @@ function* bundleHashParts(files) {
|
|
|
15072
15091
|
yield new TextEncoder().encode("\x00");
|
|
15073
15092
|
}
|
|
15074
15093
|
function hashBundleFiles(files) {
|
|
15075
|
-
const hash =
|
|
15094
|
+
const hash = createHash4(CONTENT_HASH_ALGORITHM);
|
|
15076
15095
|
for (const part of bundleHashParts(files))
|
|
15077
15096
|
hash.update(part);
|
|
15078
15097
|
return hash.digest("hex");
|
|
@@ -15416,8 +15435,8 @@ function assertAgentPolicyCollections(policy) {
|
|
|
15416
15435
|
text(source.path);
|
|
15417
15436
|
requireBound(source.sha256 === null || typeof source.sha256 === "string" && /^[a-f0-9]{64}$/.test(source.sha256));
|
|
15418
15437
|
if (source.hashMode !== undefined)
|
|
15419
|
-
requireBound(["bytes", "path-bytes", "claude-plugin-registry", "claude-marketplace-registry", "claude-settings-v1", "claude-settings-v2", "claude-marketplace-registry-v2"].includes(source.hashMode) && source.format === undefined && source.fields === undefined && (source.hashMode === "bytes" || source.sha256 !== null));
|
|
15420
|
-
if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2")
|
|
15438
|
+
requireBound(["bytes", "path-bytes", "claude-plugin-registry", "claude-marketplace-registry", "claude-settings-v1", "claude-settings-v2", "claude-settings-v3", "claude-marketplace-registry-v2"].includes(source.hashMode) && source.format === undefined && source.fields === undefined && (source.hashMode === "bytes" || source.sha256 !== null));
|
|
15439
|
+
if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3")
|
|
15421
15440
|
requireBound(agent === "claude" && value.agent === "claude" && value.method === "reviewed" && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute22(source.path) && resolve22(source.path) === source.path && basename2(source.path) === "settings.json");
|
|
15422
15441
|
if (source.hashMode === "claude-marketplace-registry" || source.hashMode === "claude-marketplace-registry-v2")
|
|
15423
15442
|
requireBound(agent === "claude" && value.agent === "claude" && value.method === "reviewed" && !/[\x00-\x1f\x7f]/.test(source.path) && isAbsolute22(source.path) && resolve22(source.path) === source.path && basename2(source.path) === "known_marketplaces.json");
|
|
@@ -15908,11 +15927,11 @@ function hashDiscoveryPathFile(path, budget, changes) {
|
|
|
15908
15927
|
const after = trace(path);
|
|
15909
15928
|
if (JSON.stringify(before.rows) !== JSON.stringify(after.rows) || before.file !== after.file)
|
|
15910
15929
|
throw new Error("Discovery path input changed during read");
|
|
15911
|
-
return
|
|
15930
|
+
return createHash42("sha256").update(JSON.stringify({ version: 1, path, rows: before.rows, content })).digest("hex");
|
|
15912
15931
|
}
|
|
15913
15932
|
var package_default = {
|
|
15914
15933
|
name: "@hasna/skills",
|
|
15915
|
-
version: "0.9.
|
|
15934
|
+
version: "0.9.17",
|
|
15916
15935
|
description: "Skills library for AI coding agents",
|
|
15917
15936
|
type: "module",
|
|
15918
15937
|
bin: {
|
|
@@ -16767,7 +16786,7 @@ function assertClaudeHookEventsReplacement(before, replacement, events) {
|
|
|
16767
16786
|
need2(JSON.stringify(withoutOwnedEvents(original)) === JSON.stringify(withoutOwnedEvents(next)), `${label} update changed settings outside hooks.${label}`);
|
|
16768
16787
|
}
|
|
16769
16788
|
var PERSISTED_EFFORT_LEVELS = new Set(["low", "medium", "high", "xhigh"]);
|
|
16770
|
-
function inferencePreference(key, child) {
|
|
16789
|
+
function inferencePreference(key, child, version) {
|
|
16771
16790
|
if (key === "skipDangerousModePermissionPrompt") {
|
|
16772
16791
|
need2(child.kind === "literal" && (child.value === "true" || child.value === "false"), "contains an invalid dialog acknowledgment");
|
|
16773
16792
|
return false;
|
|
@@ -16780,7 +16799,7 @@ function inferencePreference(key, child) {
|
|
|
16780
16799
|
return false;
|
|
16781
16800
|
need2(child.kind === "object", "requires an object of model preferences");
|
|
16782
16801
|
child.entries = child.entries.filter(([model, preferences]) => {
|
|
16783
|
-
if (!BUILTIN_MODEL_SELECTIONS.has(model) || preferences.kind !== "object" || preferences.entries.length !== 1 || preferences.entries[0][0] !== "effortLevel")
|
|
16802
|
+
if (version === 2 && !BUILTIN_MODEL_SELECTIONS.has(model) || preferences.kind !== "object" || preferences.entries.length !== 1 || preferences.entries[0][0] !== "effortLevel")
|
|
16784
16803
|
return true;
|
|
16785
16804
|
const effort = preferences.entries[0][1];
|
|
16786
16805
|
need2(effort.kind === "string" && PERSISTED_EFFORT_LEVELS.has(effort.value), "contains an invalid persisted model effort level");
|
|
@@ -16791,7 +16810,7 @@ function inferencePreference(key, child) {
|
|
|
16791
16810
|
function settingsDigest(text2, version = 1) {
|
|
16792
16811
|
const value = parse3(text2);
|
|
16793
16812
|
value.entries = value.entries.filter(([key, child]) => {
|
|
16794
|
-
if (version
|
|
16813
|
+
if (version !== 1 && inferencePreference(key, child, version))
|
|
16795
16814
|
return false;
|
|
16796
16815
|
if (key === "model") {
|
|
16797
16816
|
need2(child.kind === "string", "contains an invalid model selection");
|
|
@@ -16829,6 +16848,16 @@ function hashClaudeSettingsReplacementV2(text2, budget) {
|
|
|
16829
16848
|
budget.remaining -= bytes;
|
|
16830
16849
|
return settingsDigest(text2, 2);
|
|
16831
16850
|
}
|
|
16851
|
+
function captureClaudeSettingsV3(path, budget = { remaining: 256 * 1024 * 1024 }) {
|
|
16852
|
+
return { path, hashMode: "claude-settings-v3", sha256: settingsDigest(readSettings(path, budget), 3) };
|
|
16853
|
+
}
|
|
16854
|
+
function hashClaudeSettingsReplacementV3(text2, budget) {
|
|
16855
|
+
need2(typeof text2 === "string", "requires settings text");
|
|
16856
|
+
const bytes = Buffer.byteLength(text2);
|
|
16857
|
+
need2(bytes <= CLAUDE_SETTINGS_WITNESS_LIMITS.bytes && Number.isSafeInteger(budget.remaining) && budget.remaining >= bytes, "exceeds its byte limit");
|
|
16858
|
+
budget.remaining -= bytes;
|
|
16859
|
+
return settingsDigest(text2, 3);
|
|
16860
|
+
}
|
|
16832
16861
|
var sha = (data) => createHash10("sha256").update(data).digest("hex");
|
|
16833
16862
|
var ROOTS = NATIVE_SKILL_ROOTS;
|
|
16834
16863
|
function assertSafePath(path) {
|
|
@@ -17282,6 +17311,8 @@ function applyAgentIntegration(plan) {
|
|
|
17282
17311
|
parseManagedSkillPolicy(change.after);
|
|
17283
17312
|
const aliases = plan.rootAliases ?? [];
|
|
17284
17313
|
recheckRootAliases(aliases);
|
|
17314
|
+
for (const agent of plan.retainedReviewChecks?.agents ?? [])
|
|
17315
|
+
assertManagedAgentBridge(agent, { ...plan.retainedReviewChecks, dataDir: plan.dataDir });
|
|
17285
17316
|
for (const agent of plan.managedAgentChecks?.agents ?? [])
|
|
17286
17317
|
assertManagedAgentBridge(agent, { home: plan.managedAgentChecks.home, dataDir: plan.dataDir, projectDir: plan.managedAgentChecks.home });
|
|
17287
17318
|
for (const binding of plan.discoveryBefore ?? []) {
|
|
@@ -18295,13 +18326,17 @@ function unchanged2(before) {
|
|
|
18295
18326
|
const after = snapshot(before.file, before.privateFile, before.readOnlyPackage ? { readOnlyPackage: true } : {});
|
|
18296
18327
|
need4(sameStat(before.stat, after.stat) && after.sha256 === before.sha256, "INPUT_CHANGED");
|
|
18297
18328
|
}
|
|
18298
|
-
var
|
|
18329
|
+
var events = { UserPromptSubmit: ["userPromptSubmit", "user_prompt_submit"], SessionStart: ["sessionStart", "session_start"], SubagentStart: ["subagentStart", "subagent_start"] };
|
|
18330
|
+
var matchedEvents = { ...events, PreToolUse: ["preToolUse", "pre_tool_use"] };
|
|
18299
18331
|
var proofs = new WeakMap;
|
|
18332
|
+
var sha2 = (text2) => createHash15("sha256").update(text2).digest("hex");
|
|
18333
|
+
var proofs2 = new WeakMap;
|
|
18300
18334
|
function assertCodexHookDiscoveryRecovery(binding, witness) {
|
|
18301
|
-
const proof =
|
|
18335
|
+
const proof = proofs2.get(witness);
|
|
18302
18336
|
need4(!!proof && binding.agent === "codex" && witness.discoverySha256 === sha2(JSON.stringify(binding)), "RECONCILE_DISCOVERY_RECOVERY_CHANGED");
|
|
18303
18337
|
unchanged2(proof.before);
|
|
18304
18338
|
unchanged2(proof.current);
|
|
18339
|
+
proof.recheckAdditions();
|
|
18305
18340
|
}
|
|
18306
18341
|
function verifiesCodexHookDiscoverySource(binding, source, currentDigest, witness) {
|
|
18307
18342
|
if (!witness || source.path !== witness.configPath || source.hashMode !== "bytes")
|
|
@@ -18343,13 +18378,13 @@ function read2(path, changes) {
|
|
|
18343
18378
|
return readFileSync16(path, "utf8");
|
|
18344
18379
|
}
|
|
18345
18380
|
function projected(source, changes, budget = discoveryByteBudget()) {
|
|
18346
|
-
if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2") {
|
|
18381
|
+
if (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3") {
|
|
18347
18382
|
if (source.format !== undefined || source.fields !== undefined || source.managedPlugins !== undefined || typeof source.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(source.sha256))
|
|
18348
18383
|
throw new Error("Claude settings witnesses require exact typed metadata");
|
|
18349
|
-
const current2 = (source.hashMode === "claude-settings-v2" ? captureClaudeSettingsV2 : captureClaudeSettings)(source.path, budget).sha256;
|
|
18384
|
+
const current2 = (source.hashMode === "claude-settings-v3" ? captureClaudeSettingsV3 : source.hashMode === "claude-settings-v2" ? captureClaudeSettingsV2 : captureClaudeSettings)(source.path, budget).sha256;
|
|
18350
18385
|
if (current2 !== source.sha256)
|
|
18351
18386
|
throw new Error(`Native discovery input changed; run skills hook install with a fresh discovery review: ${source.path}`);
|
|
18352
|
-
return changes?.has(source.path) ? (source.hashMode === "claude-settings-v2" ? hashClaudeSettingsReplacementV2 : hashClaudeSettingsReplacement)(changes.get(source.path), budget) : current2;
|
|
18387
|
+
return changes?.has(source.path) ? (source.hashMode === "claude-settings-v3" ? hashClaudeSettingsReplacementV3 : source.hashMode === "claude-settings-v2" ? hashClaudeSettingsReplacementV2 : hashClaudeSettingsReplacement)(changes.get(source.path), budget) : current2;
|
|
18353
18388
|
}
|
|
18354
18389
|
if (source.hashMode === "claude-marketplace-registry" || source.hashMode === "claude-marketplace-registry-v2") {
|
|
18355
18390
|
if (source.format !== undefined || source.fields !== undefined || source.managedPlugins !== undefined || typeof source.sha256 !== "string" || !/^[a-f0-9]{64}$/.test(source.sha256) || changes?.has(source.path))
|
|
@@ -18411,7 +18446,7 @@ function verifyAgentDiscovery(binding, codexRecovery) {
|
|
|
18411
18446
|
safe4(root2);
|
|
18412
18447
|
}
|
|
18413
18448
|
function assertMarketplaceBinding(binding, source) {
|
|
18414
|
-
if ((source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2") && (binding.agent !== "claude" || binding.method !== "reviewed" || basename8(source.path) !== "settings.json"))
|
|
18449
|
+
if ((source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3") && (binding.agent !== "claude" || binding.method !== "reviewed" || basename8(source.path) !== "settings.json"))
|
|
18415
18450
|
throw new Error("Claude settings witnesses require explicit reviewed Claude configuration");
|
|
18416
18451
|
if ((source.hashMode === "claude-marketplace-registry" || source.hashMode === "claude-marketplace-registry-v2") && (binding.agent !== "claude" || binding.method !== "reviewed"))
|
|
18417
18452
|
throw new Error("Claude marketplace witnesses require explicit reviewed Claude discovery");
|
|
@@ -18420,7 +18455,10 @@ function rebindAgentDiscovery(binding, changes) {
|
|
|
18420
18455
|
const budget = discoveryByteBudget();
|
|
18421
18456
|
return { ...binding, sources: binding.sources.map((source) => {
|
|
18422
18457
|
assertMarketplaceBinding(binding, source);
|
|
18423
|
-
|
|
18458
|
+
const sha2562 = projected(source, changes, budget);
|
|
18459
|
+
if (!changes.has(source.path) && sha2562 !== source.sha256)
|
|
18460
|
+
throw new Error(`Native discovery input changed; run skills hook install with a fresh discovery review: ${source.path}`);
|
|
18461
|
+
return { ...source, sha256: sha2562 };
|
|
18424
18462
|
}) };
|
|
18425
18463
|
}
|
|
18426
18464
|
function assertProjectDiscovery(agent, directories, home, canonical4 = resolve14, reviewed) {
|
|
@@ -18532,6 +18570,19 @@ function resolveAgentDiscovery(options) {
|
|
|
18532
18570
|
}
|
|
18533
18571
|
}
|
|
18534
18572
|
}
|
|
18573
|
+
if (options.reviewed === undefined && options.retainedReview !== undefined) {
|
|
18574
|
+
const retained = options.retainedReview;
|
|
18575
|
+
if (retained.agent !== agent || retained.method !== "reviewed")
|
|
18576
|
+
unresolved("invalid retained review");
|
|
18577
|
+
verifyAgentDiscovery(retained);
|
|
18578
|
+
if (!retained.sources.some((source) => source.path === canonical4(configPath) && source.format === undefined && source.fields === undefined))
|
|
18579
|
+
unresolved("retained review is missing the full agent configuration source");
|
|
18580
|
+
if (retained.sources.some((source) => (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3") && source.path !== canonical4(configPath)))
|
|
18581
|
+
unresolved("retained settings witness names another configuration source");
|
|
18582
|
+
if (sources.some((source) => !retained.sources.some((saved) => isDeepStrictEqual3(saved, source))) || [...roots].some((root2) => !retained.roots.includes(root2)) || agent === "gemini" && JSON.stringify(retained.builtinNames) !== JSON.stringify(builtinNames))
|
|
18583
|
+
unresolved("retained review is missing current runtime discovery coverage");
|
|
18584
|
+
return retained;
|
|
18585
|
+
}
|
|
18535
18586
|
if (options.reviewed && (options.reviewed.version !== 1 || !Array.isArray(options.reviewed.agents) || options.reviewed.agents.some((item) => !item || typeof item !== "object") || new Set(options.reviewed.agents.map((item) => item.agent)).size !== options.reviewed.agents.length))
|
|
18536
18587
|
throw new Error("Invalid --discovery-inputs version or agents");
|
|
18537
18588
|
const review = options.reviewed?.agents.find((item) => item.agent === agent);
|
|
@@ -18542,7 +18593,7 @@ function resolveAgentDiscovery(options) {
|
|
|
18542
18593
|
if (review.sources.some((source) => source.format !== undefined || source.fields !== undefined))
|
|
18543
18594
|
throw new Error("Explicit discovery reviews require full source-file hashes");
|
|
18544
18595
|
verifyAgentDiscovery(supplied);
|
|
18545
|
-
if (review.sources.some((source) => (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2") && source.path !== canonical4(configPath)))
|
|
18596
|
+
if (review.sources.some((source) => (source.hashMode === "claude-settings-v1" || source.hashMode === "claude-settings-v2" || source.hashMode === "claude-settings-v3") && source.path !== canonical4(configPath)))
|
|
18546
18597
|
throw new Error("Semantic settings witnesses must name the configured Claude configuration source");
|
|
18547
18598
|
if (!review.sources.some((source) => source.path === canonical4(configPath)))
|
|
18548
18599
|
throw new Error("Discovery review must include the agent configuration source");
|
|
@@ -39052,8 +39103,8 @@ var SESSION_RECONCILIATION_PLAN_TTL_MS = 5 * 60 * 1000;
|
|
|
39052
39103
|
// src/lib/claude-skills-coordination.ts
|
|
39053
39104
|
function coordinateClaudeSettings(before, replacement, options) {
|
|
39054
39105
|
const prior = JSON.parse(before ?? "{}"), next = JSON.parse(replacement);
|
|
39055
|
-
const
|
|
39056
|
-
const changed = [...
|
|
39106
|
+
const events2 = new Set([...Object.keys(prior.hooks ?? {}), ...Object.keys(next.hooks ?? {})]);
|
|
39107
|
+
const changed = [...events2].filter((event) => JSON.stringify(prior.hooks?.[event]) !== JSON.stringify(next.hooks?.[event]));
|
|
39057
39108
|
const selected = changed.length ? changed : ["PreToolUse"];
|
|
39058
39109
|
const plan = planClaudeHookEventsUpdate({
|
|
39059
39110
|
...options,
|
|
@@ -39139,9 +39190,9 @@ function getTargetSettingsDir(target) {
|
|
|
39139
39190
|
}
|
|
39140
39191
|
function getGlobalSettingsPathOverride(target) {
|
|
39141
39192
|
if (target === "codex")
|
|
39142
|
-
return process.env.HASNA_HOOKS_CODEX_SETTINGS_PATH;
|
|
39193
|
+
return process.env.HASNA_HOOKS_CODEX_SETTINGS_PATH ?? (process.env.CODEX_HOME ? resolve7(process.env.CODEX_HOME, "hooks.json") : undefined);
|
|
39143
39194
|
if (target === "claude")
|
|
39144
|
-
return process.env.HASNA_HOOKS_CLAUDE_SETTINGS_PATH;
|
|
39195
|
+
return process.env.HASNA_HOOKS_CLAUDE_SETTINGS_PATH ?? (process.env.CLAUDE_CONFIG_DIR ? resolve7(process.env.CLAUDE_CONFIG_DIR, "settings.json") : undefined);
|
|
39145
39196
|
if (target === "gemini")
|
|
39146
39197
|
return process.env.HASNA_HOOKS_GEMINI_SETTINGS_PATH;
|
|
39147
39198
|
return;
|
|
@@ -39185,7 +39236,7 @@ function readSettings2(scope = "global", target = "claude") {
|
|
|
39185
39236
|
return value;
|
|
39186
39237
|
}
|
|
39187
39238
|
} catch (error61) {
|
|
39188
|
-
console.warn(
|
|
39239
|
+
console.warn("[hooks] Cannot read hook settings; repair the selected file before installation.");
|
|
39189
39240
|
}
|
|
39190
39241
|
const empty = {};
|
|
39191
39242
|
if (!existsSync10(path))
|
|
@@ -39197,10 +39248,16 @@ function settingsIdentity(path) {
|
|
|
39197
39248
|
return realpathSync5(path);
|
|
39198
39249
|
return existsSync10(dirname7(path)) ? join10(realpathSync5(dirname7(path)), basename3(path)) : resolve7(path);
|
|
39199
39250
|
}
|
|
39200
|
-
function writeSettings(settings, scope = "global", target = "claude") {
|
|
39251
|
+
function writeSettings(settings, scope = "global", target = "claude", expectedSettingsSha256) {
|
|
39201
39252
|
const path = getSettingsPath(scope, target);
|
|
39202
39253
|
if (target === "codex")
|
|
39203
|
-
return writeCodexSettings(path, settings);
|
|
39254
|
+
return writeCodexSettings(path, settings, expectedSettingsSha256);
|
|
39255
|
+
const captured = settingsPreimages.get(settings);
|
|
39256
|
+
if (expectedSettingsSha256 !== undefined) {
|
|
39257
|
+
if (!captured || captured.path !== path)
|
|
39258
|
+
throw new Error("Hook settings require a valid captured preimage.");
|
|
39259
|
+
assertSettingsPreimage(captured.text, expectedSettingsSha256);
|
|
39260
|
+
}
|
|
39204
39261
|
if (target === "claude" && settingsIdentity(path) === settingsIdentity(join10(homedir5(), ".claude/settings.json"))) {
|
|
39205
39262
|
const before = settingsPreimages.get(settings);
|
|
39206
39263
|
if (!before || before.path !== path)
|
|
@@ -39212,6 +39269,8 @@ function writeSettings(settings, scope = "global", target = "claude") {
|
|
|
39212
39269
|
`, { home: homedir5() }))
|
|
39213
39270
|
return;
|
|
39214
39271
|
}
|
|
39272
|
+
if (expectedSettingsSha256 !== undefined)
|
|
39273
|
+
return writeHookSettingsPreimage(path, settings, captured.text);
|
|
39215
39274
|
const dir = dirname7(path);
|
|
39216
39275
|
if (!existsSync10(dir)) {
|
|
39217
39276
|
mkdirSync6(dir, { recursive: true });
|
|
@@ -39333,7 +39392,7 @@ function detectConflict(name, scope, target) {
|
|
|
39333
39392
|
const meta3 = isNativeSafetyName(name) ? getHook(name) : resolveHookMeta(name);
|
|
39334
39393
|
if (!meta3 || !meta3.matcher)
|
|
39335
39394
|
return;
|
|
39336
|
-
const
|
|
39395
|
+
const events2 = new Set(getHookEvents(meta3));
|
|
39337
39396
|
const registered = getRegisteredHooksForTarget(scope, target);
|
|
39338
39397
|
for (const existingName of registered) {
|
|
39339
39398
|
if (existingName === name)
|
|
@@ -39341,7 +39400,7 @@ function detectConflict(name, scope, target) {
|
|
|
39341
39400
|
const existing = getHook(existingName);
|
|
39342
39401
|
if (!existing || !existing.matcher)
|
|
39343
39402
|
continue;
|
|
39344
|
-
if (!getHookEvents(existing).some((event) =>
|
|
39403
|
+
if (!getHookEvents(existing).some((event) => events2.has(event)))
|
|
39345
39404
|
continue;
|
|
39346
39405
|
if (matchersOverlap(meta3.matcher.toLowerCase(), existing.matcher.toLowerCase())) {
|
|
39347
39406
|
return `conflicts with '${existingName}' (same event ${meta3.event}, overlapping matcher '${existing.matcher}')`;
|
|
@@ -39369,7 +39428,7 @@ function detectRewriteConflict(name, scope, target, lookup = getHook) {
|
|
|
39369
39428
|
}
|
|
39370
39429
|
return;
|
|
39371
39430
|
}
|
|
39372
|
-
function installForTarget(name, scope, overwrite, target, profile, codewithMode = "fragment", codewithConfigPath, mementos) {
|
|
39431
|
+
function installForTarget(name, scope, overwrite, target, profile, codewithMode = "fragment", codewithConfigPath, mementos, expectedSettingsSha256) {
|
|
39373
39432
|
const shortName = shortHookName(name);
|
|
39374
39433
|
if (isNativeSafetyName(shortName) && (profile || target === "gemini" || target === "codewith" && codewithMode === "write")) {
|
|
39375
39434
|
return { hook: shortName, success: false, error: "Bundled native safety guards support Claude/Codex and managed Codewith fragments, without registry profiles.", target };
|
|
@@ -39436,7 +39495,7 @@ function installForTarget(name, scope, overwrite, target, profile, codewithMode
|
|
|
39436
39495
|
if (rewriteConflict) {
|
|
39437
39496
|
return { hook: shortName, success: false, error: `Refused: ${rewriteConflict}`, scope, target };
|
|
39438
39497
|
}
|
|
39439
|
-
registerHook(shortName, scope, target, profile, mementos);
|
|
39498
|
+
registerHook(shortName, scope, target, profile, mementos, (settings, selectedScope, selectedTarget) => writeSettings(settings, selectedScope, selectedTarget, expectedSettingsSha256));
|
|
39440
39499
|
return {
|
|
39441
39500
|
hook: shortName,
|
|
39442
39501
|
success: true,
|
|
@@ -39455,7 +39514,10 @@ function installForTarget(name, scope, overwrite, target, profile, codewithMode
|
|
|
39455
39514
|
}
|
|
39456
39515
|
}
|
|
39457
39516
|
function installHook(name, options = {}) {
|
|
39458
|
-
const { scope = "global", overwrite = false, target = "claude", profile, codewithMode = "fragment", codewithConfigPath, mementos } = options;
|
|
39517
|
+
const { scope = "global", overwrite = false, target = "claude", profile, codewithMode = "fragment", codewithConfigPath, mementos, expectedSettingsSha256 } = options;
|
|
39518
|
+
if (expectedSettingsSha256 !== undefined && (!/^(?:absent|[a-f0-9]{64})$/.test(expectedSettingsSha256) || !isNativeSafetyName(shortHookName(name)) || target !== "codex" && target !== "claude")) {
|
|
39519
|
+
return { hook: name, success: false, target, error: "An exact settings predecessor requires a Claude or Codex native safety guard." };
|
|
39520
|
+
}
|
|
39459
39521
|
try {
|
|
39460
39522
|
validateMementosRegistration(name, mementos);
|
|
39461
39523
|
} catch (error61) {
|
|
@@ -39496,9 +39558,26 @@ function installHook(name, options = {}) {
|
|
|
39496
39558
|
}
|
|
39497
39559
|
return { ...claudeResult, target: "all" };
|
|
39498
39560
|
}
|
|
39499
|
-
return installForTarget(name, scope, overwrite, target, profile, codewithMode, codewithConfigPath, mementos);
|
|
39561
|
+
return installForTarget(name, scope, overwrite, target, profile, codewithMode, codewithConfigPath, mementos, expectedSettingsSha256);
|
|
39500
39562
|
}
|
|
39501
|
-
function
|
|
39563
|
+
function previewNativeSafetyRegistration(target, scope = "global") {
|
|
39564
|
+
const conflict = detectRewriteConflict("trash-guard", scope, target);
|
|
39565
|
+
if (conflict)
|
|
39566
|
+
throw new Error("Conflicting input rewriter; reconcile it before installing the safety guard.");
|
|
39567
|
+
let result;
|
|
39568
|
+
registerHook("trash-guard", scope, target, undefined, undefined, (settings) => {
|
|
39569
|
+
const before = target === "codex" ? capturedCodexSettings(settings) : settingsPreimages.get(settings)?.text;
|
|
39570
|
+
if (before === undefined)
|
|
39571
|
+
throw new Error("Hook settings require a valid captured preimage.");
|
|
39572
|
+
const command = installedNativeSafetyCommand("trash-guard");
|
|
39573
|
+
result = { before, after: JSON.stringify(settings, null, 2) + `
|
|
39574
|
+
`, command };
|
|
39575
|
+
});
|
|
39576
|
+
if (!result)
|
|
39577
|
+
throw new Error("Native safety registration could not be rendered.");
|
|
39578
|
+
return result;
|
|
39579
|
+
}
|
|
39580
|
+
function registerHook(name, scope = "global", target = "claude", profile, mementos, write = writeSettings) {
|
|
39502
39581
|
const meta3 = isNativeSafetyName(name) ? getHook(name) : resolveHookMeta(name);
|
|
39503
39582
|
if (!meta3)
|
|
39504
39583
|
return;
|
|
@@ -39527,7 +39606,7 @@ function registerHook(name, scope = "global", target = "claude", profile, mement
|
|
|
39527
39606
|
}
|
|
39528
39607
|
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
39608
|
const hookCommand = isNativeSafetyName(name) ? installedNativeSafetyCommand(name) : (profile ? `hooks run ${name} --profile ${profile}` : `hooks run ${name}`) + mementosCommandSuffix(mementos ?? retained);
|
|
39530
|
-
if (target === "codex") {
|
|
39609
|
+
if (target === "codex" || target === "claude" && isNativeSafetyName(name)) {
|
|
39531
39610
|
const positions = [];
|
|
39532
39611
|
for (const [event, groups] of Object.entries(settings.hooks)) {
|
|
39533
39612
|
for (const entry of groups) {
|
|
@@ -39553,7 +39632,7 @@ function registerHook(name, scope = "global", target = "claude", profile, mement
|
|
|
39553
39632
|
if (typeof meta3.timeoutSeconds === "number" && meta3.timeoutSeconds > 0)
|
|
39554
39633
|
entry.hooks[index].timeout = meta3.timeoutSeconds;
|
|
39555
39634
|
}
|
|
39556
|
-
|
|
39635
|
+
write(settings, scope, target);
|
|
39557
39636
|
return;
|
|
39558
39637
|
}
|
|
39559
39638
|
}
|
|
@@ -39573,7 +39652,7 @@ function registerHook(name, scope = "global", target = "claude", profile, mement
|
|
|
39573
39652
|
}
|
|
39574
39653
|
settings.hooks[eventKey].push(entry);
|
|
39575
39654
|
}
|
|
39576
|
-
|
|
39655
|
+
write(settings, scope, target);
|
|
39577
39656
|
}
|
|
39578
39657
|
function removeHookFromAllEvents(settings, name) {
|
|
39579
39658
|
if (!settings.hooks)
|
|
@@ -39807,7 +39886,7 @@ function uninstallHook(name, scope = "global", target = "claude") {
|
|
|
39807
39886
|
// src/index.ts
|
|
39808
39887
|
init_profiles();
|
|
39809
39888
|
init_config();
|
|
39810
|
-
import { existsSync as
|
|
39889
|
+
import { existsSync as existsSync15, readFileSync as readFileSync26 } from "fs";
|
|
39811
39890
|
|
|
39812
39891
|
// src/lib/transport.ts
|
|
39813
39892
|
init_transport();
|
|
@@ -40046,16 +40125,16 @@ async function installManifestUrl(url2) {
|
|
|
40046
40125
|
return { name, version: manifest.version, kind: "url", source: url2, dir, scriptPath };
|
|
40047
40126
|
}
|
|
40048
40127
|
// src/lib/sync.ts
|
|
40049
|
-
import { createHash as
|
|
40128
|
+
import { createHash as createHash6 } from "crypto";
|
|
40050
40129
|
import { readFileSync as readFileSync15 } from "fs";
|
|
40051
40130
|
init_db();
|
|
40052
40131
|
init_store();
|
|
40053
40132
|
init_manifest();
|
|
40054
40133
|
function sha256OfText(text3) {
|
|
40055
|
-
return
|
|
40134
|
+
return createHash6("sha256").update(text3).digest("hex");
|
|
40056
40135
|
}
|
|
40057
40136
|
function sha256OfPath(path) {
|
|
40058
|
-
return
|
|
40137
|
+
return createHash6("sha256").update(readFileSync15(path)).digest("hex");
|
|
40059
40138
|
}
|
|
40060
40139
|
function collectBundledCatalog() {
|
|
40061
40140
|
const out = [];
|
|
@@ -40977,6 +41056,933 @@ async function provisionCloudflareResources(options) {
|
|
|
40977
41056
|
];
|
|
40978
41057
|
return result;
|
|
40979
41058
|
}
|
|
41059
|
+
// src/lib/codex-safety-trust.ts
|
|
41060
|
+
import { createHash as createHash12, randomUUID as randomUUID3 } from "crypto";
|
|
41061
|
+
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";
|
|
41062
|
+
import { dirname as dirname21, isAbsolute as isAbsolute18, join as join24, relative as relative2 } from "path";
|
|
41063
|
+
import { homedir as homedir7 } from "os";
|
|
41064
|
+
|
|
41065
|
+
// src/lib/codex-safety-check.ts
|
|
41066
|
+
import { spawn, execFile as execFile2 } from "child_process";
|
|
41067
|
+
import { createHash as createHash7 } from "crypto";
|
|
41068
|
+
import { constants as constants4, openSync as openSync6, closeSync as closeSync6, fstatSync as fstatSync5, lstatSync as lstatSync10, readFileSync as readFileSync20, realpathSync as realpathSync9 } from "fs";
|
|
41069
|
+
import { dirname as dirname20, isAbsolute as isAbsolute17, resolve as resolve17 } from "path";
|
|
41070
|
+
import { promisify } from "util";
|
|
41071
|
+
|
|
41072
|
+
// src/native-safety.ts
|
|
41073
|
+
import { execFile } from "child_process";
|
|
41074
|
+
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";
|
|
41075
|
+
import { homedir as homedir6 } from "os";
|
|
41076
|
+
import { dirname as dirname19, isAbsolute as isAbsolute15, join as join20, resolve as resolve15 } from "path";
|
|
41077
|
+
class NativeSafetyError extends Error {
|
|
41078
|
+
code;
|
|
41079
|
+
reason;
|
|
41080
|
+
constructor(code2, reason) {
|
|
41081
|
+
super(`Native deletion safety refused (${code2}). Run the reviewed Hooks safety setup before using shell or patch tools.`);
|
|
41082
|
+
this.code = code2;
|
|
41083
|
+
this.reason = reason;
|
|
41084
|
+
this.name = "NativeSafetyError";
|
|
41085
|
+
}
|
|
41086
|
+
}
|
|
41087
|
+
var executionSchema = "hasna.hooks.execution.v1";
|
|
41088
|
+
var owner2 = (uid2) => uid2 === 0 || uid2 === process.getuid?.();
|
|
41089
|
+
function safeDirectory(path) {
|
|
41090
|
+
for (let cursor = path;; cursor = dirname19(cursor)) {
|
|
41091
|
+
const stat2 = lstatSync9(cursor);
|
|
41092
|
+
if (!stat2.isDirectory() || !owner2(stat2.uid) || stat2.mode & 18)
|
|
41093
|
+
throw new NativeSafetyError("binding_invalid");
|
|
41094
|
+
if (dirname19(cursor) === cursor)
|
|
41095
|
+
return;
|
|
41096
|
+
}
|
|
41097
|
+
}
|
|
41098
|
+
function canonicalHome(home) {
|
|
41099
|
+
if (!isAbsolute15(home) || home.includes("\x00") || realpathSync8(home) !== resolve15(home))
|
|
41100
|
+
throw new NativeSafetyError("binding_invalid");
|
|
41101
|
+
safeDirectory(home);
|
|
41102
|
+
return resolve15(home);
|
|
41103
|
+
}
|
|
41104
|
+
function parseRecord(text3, home) {
|
|
41105
|
+
const record3 = JSON.parse(text3);
|
|
41106
|
+
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")
|
|
41107
|
+
throw new NativeSafetyError("binding_invalid");
|
|
41108
|
+
const binding = nativeSafetyCommandBinding(record3.command);
|
|
41109
|
+
if (binding?.name !== "trash-guard" || binding.home !== home)
|
|
41110
|
+
throw new NativeSafetyError("binding_invalid");
|
|
41111
|
+
return record3;
|
|
41112
|
+
}
|
|
41113
|
+
async function evaluateRecord(event, record3, signal, executionAgent) {
|
|
41114
|
+
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))
|
|
41115
|
+
throw new NativeSafetyError("input_invalid");
|
|
41116
|
+
const input2 = JSON.stringify(executionAgent === undefined ? event : { ...event, hasna_execution: { schema: executionSchema, agent: executionAgent } });
|
|
41117
|
+
if (Buffer.byteLength(input2) > 1048576)
|
|
41118
|
+
throw new NativeSafetyError("input_invalid");
|
|
41119
|
+
const output2 = await new Promise((accept, reject) => {
|
|
41120
|
+
const child = execFile("/bin/sh", ["-c", record3.command], { cwd: "/", env: {}, encoding: "utf8", timeout: 8000, maxBuffer: 1048576, signal }, (error61, stdout, stderr) => {
|
|
41121
|
+
if (error61 || stderr) {
|
|
41122
|
+
reject(new NativeSafetyError("guard_failed"));
|
|
41123
|
+
return;
|
|
41124
|
+
}
|
|
41125
|
+
accept(stdout);
|
|
41126
|
+
});
|
|
41127
|
+
child.stdin?.on("error", () => {});
|
|
41128
|
+
child.stdin?.end(input2);
|
|
41129
|
+
});
|
|
41130
|
+
if (!output2)
|
|
41131
|
+
return { decision: "continue" };
|
|
41132
|
+
try {
|
|
41133
|
+
const value = JSON.parse(output2);
|
|
41134
|
+
if (Object.keys(value).join() !== "hookSpecificOutput")
|
|
41135
|
+
throw new Error;
|
|
41136
|
+
const verdict = value.hookSpecificOutput;
|
|
41137
|
+
if (verdict.hookEventName !== "PreToolUse")
|
|
41138
|
+
throw new Error;
|
|
41139
|
+
if (verdict.permissionDecision === "deny" && Object.keys(verdict).sort().join() === "hookEventName,permissionDecision,permissionDecisionReason" && typeof verdict.permissionDecisionReason === "string" && verdict.permissionDecisionReason.trim())
|
|
41140
|
+
return { decision: "deny", reason: verdict.permissionDecisionReason };
|
|
41141
|
+
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))
|
|
41142
|
+
return { decision: "rewrite", command: verdict.updatedInput.command };
|
|
41143
|
+
} catch {}
|
|
41144
|
+
throw new NativeSafetyError("guard_failed");
|
|
41145
|
+
}
|
|
41146
|
+
async function verifyNativeSafetyCommand(command, home = homedir6()) {
|
|
41147
|
+
home = canonicalHome(home);
|
|
41148
|
+
const record3 = parseRecord(JSON.stringify({ schema: "hasna.hooks.native-safety.v1", harness: "sumi", command }), home);
|
|
41149
|
+
const verdict = await evaluateRecord({ hook_event_name: "PreToolUse", tool_name: "Bash", cwd: home, tool_input: { command: "rm -- .hasna-trash-verification-only" } }, record3);
|
|
41150
|
+
if (verdict.decision !== "rewrite")
|
|
41151
|
+
throw new NativeSafetyError("guard_failed", verdict.decision === "deny" ? verdict.reason.slice(0, 512) : undefined);
|
|
41152
|
+
}
|
|
41153
|
+
|
|
41154
|
+
// src/lib/codex-safety-check.ts
|
|
41155
|
+
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"]);
|
|
41156
|
+
var matcher = "^(Bash|apply_patch|ApplyPatch|functions\\.apply_patch)$";
|
|
41157
|
+
|
|
41158
|
+
class CodexSafetyError extends Error {
|
|
41159
|
+
code;
|
|
41160
|
+
constructor(code2) {
|
|
41161
|
+
super(`Codex deletion safety check failed (${code2}).`);
|
|
41162
|
+
this.code = code2;
|
|
41163
|
+
this.name = "CodexSafetyError";
|
|
41164
|
+
}
|
|
41165
|
+
}
|
|
41166
|
+
function need5(condition, code2) {
|
|
41167
|
+
if (!condition)
|
|
41168
|
+
throw new CodexSafetyError(code2);
|
|
41169
|
+
}
|
|
41170
|
+
var digest3 = (bytes) => createHash7("sha256").update(bytes).digest("hex");
|
|
41171
|
+
function nativeHeader(bytes) {
|
|
41172
|
+
if (bytes.length < 64 || !["arm64", "x64"].includes(process.arch))
|
|
41173
|
+
return false;
|
|
41174
|
+
if (process.platform === "darwin") {
|
|
41175
|
+
const cpu = process.arch === "arm64" ? 16777228 : 16777223;
|
|
41176
|
+
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;
|
|
41177
|
+
}
|
|
41178
|
+
if (process.platform === "linux") {
|
|
41179
|
+
const offset = bytes.readBigUInt64LE(32), count = bytes.readUInt16LE(56);
|
|
41180
|
+
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);
|
|
41181
|
+
}
|
|
41182
|
+
return false;
|
|
41183
|
+
}
|
|
41184
|
+
function executable(command) {
|
|
41185
|
+
const found = isAbsolute17(command) ? command : Bun.which(command);
|
|
41186
|
+
need5(found, "native_unavailable");
|
|
41187
|
+
try {
|
|
41188
|
+
const path = realpathSync9(found);
|
|
41189
|
+
for (let parent = dirname20(path);; parent = dirname20(parent)) {
|
|
41190
|
+
const stat2 = lstatSync10(parent);
|
|
41191
|
+
need5(stat2.isDirectory() && [0, process.getuid?.()].includes(stat2.uid) && !(stat2.mode & 18), "native_unsafe_executable");
|
|
41192
|
+
if (parent === dirname20(parent))
|
|
41193
|
+
break;
|
|
41194
|
+
}
|
|
41195
|
+
const fd = openSync6(path, constants4.O_RDONLY | constants4.O_NOFOLLOW | constants4.O_NONBLOCK);
|
|
41196
|
+
try {
|
|
41197
|
+
const stat2 = fstatSync5(fd);
|
|
41198
|
+
need5(stat2.isFile() && [0, process.getuid?.()].includes(stat2.uid) && !(stat2.mode & 18) && !!(stat2.mode & 64) && stat2.size <= 256000000, "native_unsafe_executable");
|
|
41199
|
+
const bytes = readFileSync20(fd);
|
|
41200
|
+
need5(bytes.length <= 256000000, "native_unavailable");
|
|
41201
|
+
need5(nativeHeader(bytes), "native_executable_required");
|
|
41202
|
+
return { path, sha256: digest3(bytes) };
|
|
41203
|
+
} finally {
|
|
41204
|
+
closeSync6(fd);
|
|
41205
|
+
}
|
|
41206
|
+
} catch (error61) {
|
|
41207
|
+
if (error61 instanceof CodexSafetyError)
|
|
41208
|
+
throw error61;
|
|
41209
|
+
throw new CodexSafetyError("native_unavailable");
|
|
41210
|
+
}
|
|
41211
|
+
}
|
|
41212
|
+
async function connectCodexSafety(options) {
|
|
41213
|
+
const command = options.command ?? "codex", before = executable(command);
|
|
41214
|
+
const env = { ...process.env, HOME: options.home, NO_COLOR: "1" };
|
|
41215
|
+
for (const key of ["NODE_OPTIONS", "NODE_PATH", "BUN_OPTIONS", "BASH_ENV", "ENV", "DYLD_INSERT_LIBRARIES", "LD_PRELOAD"])
|
|
41216
|
+
delete env[key];
|
|
41217
|
+
if (options.codexHome === undefined)
|
|
41218
|
+
delete env.CODEX_HOME;
|
|
41219
|
+
else
|
|
41220
|
+
env.CODEX_HOME = options.codexHome;
|
|
41221
|
+
let version2;
|
|
41222
|
+
try {
|
|
41223
|
+
version2 = (await promisify(execFile2)(before.path, ["--version"], { env, cwd: options.home, timeout: 5000, killSignal: "SIGKILL", maxBuffer: 4096 })).stdout.trim();
|
|
41224
|
+
} catch {
|
|
41225
|
+
throw new CodexSafetyError("native_unavailable");
|
|
41226
|
+
}
|
|
41227
|
+
need5(versions.has(version2), "native_unsupported");
|
|
41228
|
+
need5(JSON.stringify(executable(command)) === JSON.stringify(before), "guard_changed");
|
|
41229
|
+
const child = spawn(before.path, ["app-server", "--strict-config", "--stdio"], { env, cwd: options.home, stdio: ["pipe", "pipe", "pipe"] });
|
|
41230
|
+
let stopped = false, buffer = Buffer.alloc(0), total = 0, nextId = 1;
|
|
41231
|
+
const pending = new Map;
|
|
41232
|
+
const fail = () => {
|
|
41233
|
+
stopped = true;
|
|
41234
|
+
for (const waiter of pending.values())
|
|
41235
|
+
waiter.reject(new CodexSafetyError("native_failed"));
|
|
41236
|
+
pending.clear();
|
|
41237
|
+
};
|
|
41238
|
+
const deadline = setTimeout(() => {
|
|
41239
|
+
fail();
|
|
41240
|
+
child.kill("SIGKILL");
|
|
41241
|
+
}, 15000);
|
|
41242
|
+
const exited = new Promise((done) => {
|
|
41243
|
+
child.once("exit", () => {
|
|
41244
|
+
fail();
|
|
41245
|
+
clearTimeout(deadline);
|
|
41246
|
+
done();
|
|
41247
|
+
});
|
|
41248
|
+
child.once("error", () => {
|
|
41249
|
+
fail();
|
|
41250
|
+
clearTimeout(deadline);
|
|
41251
|
+
done();
|
|
41252
|
+
});
|
|
41253
|
+
});
|
|
41254
|
+
child.stdin.on("error", fail);
|
|
41255
|
+
const account = (length) => {
|
|
41256
|
+
total += length;
|
|
41257
|
+
if (total > 8388608) {
|
|
41258
|
+
fail();
|
|
41259
|
+
child.kill("SIGKILL");
|
|
41260
|
+
return false;
|
|
41261
|
+
}
|
|
41262
|
+
return true;
|
|
41263
|
+
};
|
|
41264
|
+
child.stderr.on("data", (bytes) => {
|
|
41265
|
+
account(bytes.length);
|
|
41266
|
+
});
|
|
41267
|
+
child.stdout.on("data", (bytes) => {
|
|
41268
|
+
if (stopped || !account(bytes.length))
|
|
41269
|
+
return;
|
|
41270
|
+
buffer = Buffer.concat([buffer, bytes]);
|
|
41271
|
+
for (;; ) {
|
|
41272
|
+
const end = buffer.indexOf(10);
|
|
41273
|
+
if (end < 0)
|
|
41274
|
+
return;
|
|
41275
|
+
const line = buffer.subarray(0, end);
|
|
41276
|
+
buffer = buffer.subarray(end + 1);
|
|
41277
|
+
try {
|
|
41278
|
+
const value = JSON.parse(line.toString("utf8"));
|
|
41279
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
41280
|
+
throw Error();
|
|
41281
|
+
if (!Object.hasOwn(value, "id"))
|
|
41282
|
+
continue;
|
|
41283
|
+
const waiter = pending.get(value.id);
|
|
41284
|
+
if (!waiter)
|
|
41285
|
+
throw Error();
|
|
41286
|
+
pending.delete(value.id);
|
|
41287
|
+
if (value.error || !Object.hasOwn(value, "result"))
|
|
41288
|
+
waiter.reject(new CodexSafetyError("native_failed"));
|
|
41289
|
+
else
|
|
41290
|
+
waiter.resolve(value.result);
|
|
41291
|
+
} catch {
|
|
41292
|
+
fail();
|
|
41293
|
+
child.kill("SIGKILL");
|
|
41294
|
+
return;
|
|
41295
|
+
}
|
|
41296
|
+
}
|
|
41297
|
+
});
|
|
41298
|
+
const request = (method, params) => new Promise((accept, reject) => {
|
|
41299
|
+
if (stopped) {
|
|
41300
|
+
reject(new CodexSafetyError("native_failed"));
|
|
41301
|
+
return;
|
|
41302
|
+
}
|
|
41303
|
+
const id = nextId++;
|
|
41304
|
+
pending.set(id, { resolve: accept, reject });
|
|
41305
|
+
child.stdin.write(JSON.stringify({ id, method, params }) + `
|
|
41306
|
+
`);
|
|
41307
|
+
});
|
|
41308
|
+
const rpc = {
|
|
41309
|
+
version: version2,
|
|
41310
|
+
request,
|
|
41311
|
+
async close() {
|
|
41312
|
+
fail();
|
|
41313
|
+
child.stdin.end();
|
|
41314
|
+
const force = setTimeout(() => child.kill("SIGKILL"), 1000);
|
|
41315
|
+
await exited;
|
|
41316
|
+
clearTimeout(force);
|
|
41317
|
+
clearTimeout(deadline);
|
|
41318
|
+
need5(JSON.stringify(executable(command)) === JSON.stringify(before), "guard_changed");
|
|
41319
|
+
}
|
|
41320
|
+
};
|
|
41321
|
+
try {
|
|
41322
|
+
await request("initialize", { clientInfo: { name: "hooks-safety-check", version: "1" }, capabilities: { experimentalApi: true } });
|
|
41323
|
+
child.stdin.write(`{"method":"initialized"}
|
|
41324
|
+
`);
|
|
41325
|
+
return rpc;
|
|
41326
|
+
} catch {
|
|
41327
|
+
await rpc.close();
|
|
41328
|
+
throw new CodexSafetyError("native_failed");
|
|
41329
|
+
}
|
|
41330
|
+
}
|
|
41331
|
+
function supportedDiscoveryWarnings(hooks, warnings) {
|
|
41332
|
+
if (warnings.length === 0)
|
|
41333
|
+
return true;
|
|
41334
|
+
if (warnings.length > 128)
|
|
41335
|
+
return false;
|
|
41336
|
+
const allowed = new Set;
|
|
41337
|
+
const sources = new Set(hooks.map((hook) => JSON.stringify([hook?.source, hook?.sourcePath])));
|
|
41338
|
+
for (const hook of hooks) {
|
|
41339
|
+
const path = hook?.sourcePath;
|
|
41340
|
+
if (typeof path !== "string" || path.length > 4096 || !isAbsolute17(path) || path !== resolve17(dirname20(path), "hooks.json") || !["user", "project"].includes(hook.source))
|
|
41341
|
+
continue;
|
|
41342
|
+
const toml = resolve17(dirname20(path), "config.toml");
|
|
41343
|
+
if (sources.has(JSON.stringify([hook.source, toml]))) {
|
|
41344
|
+
allowed.add(`loading hooks from both ${path} and ${toml}; prefer a single representation for this layer`);
|
|
41345
|
+
}
|
|
41346
|
+
}
|
|
41347
|
+
return warnings.every((warning) => typeof warning === "string" && allowed.delete(warning));
|
|
41348
|
+
}
|
|
41349
|
+
function selectCodexSafety(discovery, configuration, home, cwd, featureEnabled, requireReady = true) {
|
|
41350
|
+
const data = discovery?.data;
|
|
41351
|
+
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");
|
|
41352
|
+
const config2 = configuration?.config;
|
|
41353
|
+
need5(config2 && typeof config2 === "object" && !Array.isArray(config2), "native_discovery_failed");
|
|
41354
|
+
need5((config2.features?.hooks ?? featureEnabled) === true, "hooks_disabled");
|
|
41355
|
+
for (const hook2 of data[0].hooks)
|
|
41356
|
+
if (typeof hook2?.command === "string") {
|
|
41357
|
+
if (/\bhasna-native-safety-v\d+\b/.test(hook2.command))
|
|
41358
|
+
need5(nativeSafetyCommandBinding(hook2.command), "guard_definition_invalid");
|
|
41359
|
+
need5(!/^(?:hooks run trash-guard|hook-trash-guard)(?:\s|$)/.test(hook2.command.trim()), "guard_definition_invalid");
|
|
41360
|
+
}
|
|
41361
|
+
const selected = data[0].hooks.filter((hook2) => typeof hook2?.command === "string" && nativeSafetyCommandBinding(hook2.command)?.name === "trash-guard");
|
|
41362
|
+
need5(selected.length !== 0, "guard_missing");
|
|
41363
|
+
need5(selected.length === 1, "guard_ambiguous");
|
|
41364
|
+
const hook = selected[0], binding = nativeSafetyCommandBinding(hook.command);
|
|
41365
|
+
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");
|
|
41366
|
+
need5(typeof hook.enabled === "boolean" && ["trusted", "untrusted", "modified"].includes(hook.trustStatus), "guard_definition_invalid");
|
|
41367
|
+
if (requireReady) {
|
|
41368
|
+
need5(hook.enabled === true, "guard_disabled");
|
|
41369
|
+
need5(hook.trustStatus === "trusted", "guard_untrusted");
|
|
41370
|
+
}
|
|
41371
|
+
return { key: hook.key, command: hook.command, currentHash: hook.currentHash, sourcePath: hook.sourcePath, enabled: hook.enabled, trustStatus: hook.trustStatus };
|
|
41372
|
+
}
|
|
41373
|
+
async function readCodexSafety(rpc, home, cwd, options = {}) {
|
|
41374
|
+
need5(versions.has(rpc.version), "native_unsupported");
|
|
41375
|
+
const hooks = await rpc.request("hooks/list", { cwds: [cwd] });
|
|
41376
|
+
const configuration = await rpc.request("config/read", { cwd, includeLayers: options.includeLayers ?? false });
|
|
41377
|
+
let enabled;
|
|
41378
|
+
if (configuration?.config?.features?.hooks === undefined) {
|
|
41379
|
+
let cursor = null, count = 0;
|
|
41380
|
+
const seen = new Set;
|
|
41381
|
+
do {
|
|
41382
|
+
const page3 = await rpc.request("experimentalFeature/list", { cursor, limit: 100 });
|
|
41383
|
+
need5(Array.isArray(page3?.data) && page3.data.length <= 100 && (page3.nextCursor === null || typeof page3.nextCursor === "string"), "native_discovery_failed");
|
|
41384
|
+
count += page3.data.length;
|
|
41385
|
+
need5(count <= 1000, "native_discovery_failed");
|
|
41386
|
+
for (const feature of page3.data)
|
|
41387
|
+
if (feature?.name === "hooks") {
|
|
41388
|
+
need5(enabled === undefined && typeof feature.enabled === "boolean", "native_discovery_failed");
|
|
41389
|
+
enabled = feature.enabled;
|
|
41390
|
+
}
|
|
41391
|
+
cursor = page3.nextCursor;
|
|
41392
|
+
if (cursor !== null) {
|
|
41393
|
+
need5(cursor.length > 0 && cursor.length <= 4096 && !seen.has(cursor) && page3.data.length > 0, "native_discovery_failed");
|
|
41394
|
+
seen.add(cursor);
|
|
41395
|
+
}
|
|
41396
|
+
} while (cursor !== null);
|
|
41397
|
+
}
|
|
41398
|
+
return { guard: selectCodexSafety(hooks, configuration, home, cwd, enabled, options.requireReady ?? true), discovery: hooks, configuration };
|
|
41399
|
+
}
|
|
41400
|
+
|
|
41401
|
+
// src/lib/codex-safety-trust.ts
|
|
41402
|
+
class CodexTrustError extends Error {
|
|
41403
|
+
code;
|
|
41404
|
+
operationId;
|
|
41405
|
+
constructor(code2, operationId) {
|
|
41406
|
+
super("Codex safety trust failed (" + code2 + ").");
|
|
41407
|
+
this.code = code2;
|
|
41408
|
+
this.operationId = operationId;
|
|
41409
|
+
this.name = "CodexTrustError";
|
|
41410
|
+
}
|
|
41411
|
+
}
|
|
41412
|
+
function need7(value, code2 = "native_changed") {
|
|
41413
|
+
if (!value)
|
|
41414
|
+
throw new CodexTrustError(code2);
|
|
41415
|
+
}
|
|
41416
|
+
var sha4 = (value) => createHash12("sha256").update(value).digest("hex");
|
|
41417
|
+
function canonical4(value) {
|
|
41418
|
+
if (Array.isArray(value))
|
|
41419
|
+
return "[" + value.map(canonical4).join(",") + "]";
|
|
41420
|
+
if (value !== null && typeof value === "object")
|
|
41421
|
+
return "{" + Object.keys(value).sort().map((key) => JSON.stringify(key) + ":" + canonical4(value[key])).join(",") + "}";
|
|
41422
|
+
return JSON.stringify(value);
|
|
41423
|
+
}
|
|
41424
|
+
function mergedConfiguration(value) {
|
|
41425
|
+
const copy = structuredClone(value);
|
|
41426
|
+
if (copy?.hooks && typeof copy.hooks === "object" && !Array.isArray(copy.hooks)) {
|
|
41427
|
+
for (const event of [
|
|
41428
|
+
"PreToolUse",
|
|
41429
|
+
"PermissionRequest",
|
|
41430
|
+
"PostToolUse",
|
|
41431
|
+
"PreCompact",
|
|
41432
|
+
"PostCompact",
|
|
41433
|
+
"SessionStart",
|
|
41434
|
+
"SessionEnd",
|
|
41435
|
+
"UserPromptSubmit",
|
|
41436
|
+
"SubagentStart",
|
|
41437
|
+
"SubagentStop",
|
|
41438
|
+
"Stop",
|
|
41439
|
+
"Interrupt"
|
|
41440
|
+
]) {
|
|
41441
|
+
if (!Object.hasOwn(copy.hooks, event))
|
|
41442
|
+
copy.hooks[event] = [];
|
|
41443
|
+
}
|
|
41444
|
+
}
|
|
41445
|
+
return canonical4(copy);
|
|
41446
|
+
}
|
|
41447
|
+
function safeDirectory2(path) {
|
|
41448
|
+
for (let p = path;; p = dirname21(p)) {
|
|
41449
|
+
const s = lstatSync15(p);
|
|
41450
|
+
need7(s.isDirectory() && [0, process.getuid?.()].includes(s.uid) && !(s.mode & 18), "preimage_unsafe");
|
|
41451
|
+
if (dirname21(p) === p)
|
|
41452
|
+
return;
|
|
41453
|
+
}
|
|
41454
|
+
}
|
|
41455
|
+
function readPrivate(path) {
|
|
41456
|
+
safeDirectory2(dirname21(path));
|
|
41457
|
+
const fd = openSync7(path, constants12.O_RDONLY | constants12.O_NOFOLLOW | constants12.O_NONBLOCK);
|
|
41458
|
+
try {
|
|
41459
|
+
const s = fstatSync6(fd);
|
|
41460
|
+
need7(s.isFile() && s.uid === process.getuid?.() && !(s.mode & 63) && s.size <= 4194304, "preimage_unsafe");
|
|
41461
|
+
const bytes = readFileSync21(fd);
|
|
41462
|
+
need7(bytes.length <= 4194304, "preimage_unsafe");
|
|
41463
|
+
return bytes;
|
|
41464
|
+
} finally {
|
|
41465
|
+
closeSync7(fd);
|
|
41466
|
+
}
|
|
41467
|
+
}
|
|
41468
|
+
function syncDirectory(path) {
|
|
41469
|
+
const fd = openSync7(path, constants12.O_RDONLY | constants12.O_DIRECTORY | constants12.O_NOFOLLOW);
|
|
41470
|
+
try {
|
|
41471
|
+
fsyncSync5(fd);
|
|
41472
|
+
} finally {
|
|
41473
|
+
closeSync7(fd);
|
|
41474
|
+
}
|
|
41475
|
+
}
|
|
41476
|
+
function exclusive(path, bytes) {
|
|
41477
|
+
const fd = openSync7(path, constants12.O_CREAT | constants12.O_EXCL | constants12.O_WRONLY | constants12.O_NOFOLLOW, 384);
|
|
41478
|
+
try {
|
|
41479
|
+
writeFileSync11(fd, bytes);
|
|
41480
|
+
fsyncSync5(fd);
|
|
41481
|
+
} finally {
|
|
41482
|
+
closeSync7(fd);
|
|
41483
|
+
}
|
|
41484
|
+
syncDirectory(dirname21(path));
|
|
41485
|
+
}
|
|
41486
|
+
async function inspect(rpc, home, cwd) {
|
|
41487
|
+
const observed = await readCodexSafety(rpc, home, cwd, { includeLayers: true, requireReady: false });
|
|
41488
|
+
const { guard, configuration, discovery } = observed;
|
|
41489
|
+
const layers = configuration?.layers;
|
|
41490
|
+
need7(Array.isArray(layers) && layers.length <= 1000, "native_config_invalid");
|
|
41491
|
+
const userLayers = layers.filter((layer2) => layer2?.name?.type === "user" && layer2.name.profile === null);
|
|
41492
|
+
need7(userLayers.length === 1, "native_config_invalid");
|
|
41493
|
+
const layer = userLayers[0], path = layer.name.file;
|
|
41494
|
+
need7(typeof path === "string" && isAbsolute18(path) && typeof layer.version === "string" && /^sha256:[a-f0-9]{64}$/.test(layer.version), "native_config_invalid");
|
|
41495
|
+
const canonicalPath = realpathSync10(path), inside = relative2(home, canonicalPath);
|
|
41496
|
+
need7(inside !== "" && !inside.startsWith("..") && !isAbsolute18(inside), "native_config_invalid");
|
|
41497
|
+
const definition = discovery.data[0].hooks.find((h) => h.key === guard.key);
|
|
41498
|
+
need7(definition.source === "user", "native_config_invalid");
|
|
41499
|
+
const before = readPrivate(canonicalPath);
|
|
41500
|
+
const rawState = layer.config?.hooks?.state?.[guard.key];
|
|
41501
|
+
need7(rawState === undefined || rawState && typeof rawState === "object" && !Array.isArray(rawState), "native_config_invalid");
|
|
41502
|
+
const desired = { ...rawState, enabled: true, trusted_hash: guard.currentHash };
|
|
41503
|
+
const planBase = {
|
|
41504
|
+
schema: "hasna.hooks.codex-safety-trust/v1",
|
|
41505
|
+
nativeVersion: rpc.version,
|
|
41506
|
+
cwd,
|
|
41507
|
+
key: guard.key,
|
|
41508
|
+
currentHash: guard.currentHash,
|
|
41509
|
+
configPath: path,
|
|
41510
|
+
configVersion: layer.version,
|
|
41511
|
+
action: guard.enabled && guard.trustStatus === "trusted" ? "unchanged" : "trust_and_enable",
|
|
41512
|
+
guardVerified: true,
|
|
41513
|
+
nativeAdoptionVerified: false
|
|
41514
|
+
};
|
|
41515
|
+
const plan = { ...planBase, planDigest: sha4(canonical4({ planBase, configuration, discovery, raw: sha4(before) })) };
|
|
41516
|
+
return { ...observed, layer, layers, canonicalPath, before, desired, plan };
|
|
41517
|
+
}
|
|
41518
|
+
async function context2(options, dependencies) {
|
|
41519
|
+
const home = realpathSync10(options.home ?? homedir7()), cwd = realpathSync10(options.cwd ?? process.cwd());
|
|
41520
|
+
need7(isAbsolute18(home) && isAbsolute18(cwd), "input_invalid");
|
|
41521
|
+
return { home, cwd, rpc: await (dependencies.connect ?? connectCodexSafety)({ ...options, home }) };
|
|
41522
|
+
}
|
|
41523
|
+
async function planCodexSafetyTrust(options = {}, dependencies = {}) {
|
|
41524
|
+
const { home, cwd, rpc } = await context2(options, dependencies);
|
|
41525
|
+
try {
|
|
41526
|
+
const before = await inspect(rpc, home, cwd);
|
|
41527
|
+
await (dependencies.verify ?? verifyNativeSafetyCommand)(before.guard.command, home);
|
|
41528
|
+
need7((await inspect(rpc, home, cwd)).plan.planDigest === before.plan.planDigest);
|
|
41529
|
+
return before.plan;
|
|
41530
|
+
} finally {
|
|
41531
|
+
await rpc.close();
|
|
41532
|
+
}
|
|
41533
|
+
}
|
|
41534
|
+
async function applyCodexSafetyTrust(options, dependencies = {}) {
|
|
41535
|
+
need7(/^[a-f0-9]{64}$/.test(options.expectedPlanDigest), "input_invalid");
|
|
41536
|
+
const selected = { ...options }, { home, cwd, rpc } = await context2(selected, dependencies);
|
|
41537
|
+
let operationId;
|
|
41538
|
+
try {
|
|
41539
|
+
const before = await inspect(rpc, home, cwd);
|
|
41540
|
+
need7(before.plan.planDigest === selected.expectedPlanDigest);
|
|
41541
|
+
await (dependencies.verify ?? verifyNativeSafetyCommand)(before.guard.command, home);
|
|
41542
|
+
need7((await inspect(rpc, home, cwd)).plan.planDigest === before.plan.planDigest);
|
|
41543
|
+
if (before.plan.action === "unchanged")
|
|
41544
|
+
return { ok: true, changed: false, ...before.plan };
|
|
41545
|
+
const expectedConfiguration = structuredClone(before.configuration);
|
|
41546
|
+
const expectedLayer = expectedConfiguration.layers.find((layer) => layer.name?.type === "user" && layer.name.profile === null);
|
|
41547
|
+
for (const config2 of [expectedConfiguration.config, expectedLayer.config]) {
|
|
41548
|
+
config2.hooks ??= {};
|
|
41549
|
+
config2.hooks.state ??= {};
|
|
41550
|
+
config2.hooks.state[before.guard.key] = before.desired;
|
|
41551
|
+
}
|
|
41552
|
+
const expectedDiscovery = structuredClone(before.discovery);
|
|
41553
|
+
const expectedGuard = expectedDiscovery.data[0].hooks.find((h) => h.key === before.guard.key);
|
|
41554
|
+
expectedGuard.enabled = true;
|
|
41555
|
+
expectedGuard.trustStatus = "trusted";
|
|
41556
|
+
operationId = randomUUID3();
|
|
41557
|
+
let directory2 = home;
|
|
41558
|
+
for (const part of [".hasna", "hooks", "native", "codex-trash-trust", operationId]) {
|
|
41559
|
+
directory2 = join24(directory2, part);
|
|
41560
|
+
try {
|
|
41561
|
+
mkdirSync13(directory2, { mode: 448 });
|
|
41562
|
+
syncDirectory(dirname21(directory2));
|
|
41563
|
+
} catch (error61) {
|
|
41564
|
+
if (error61.code !== "EEXIST")
|
|
41565
|
+
throw error61;
|
|
41566
|
+
}
|
|
41567
|
+
safeDirectory2(directory2);
|
|
41568
|
+
}
|
|
41569
|
+
exclusive(join24(directory2, "config.before"), before.before);
|
|
41570
|
+
exclusive(join24(directory2, "intent.json"), JSON.stringify({ operationId, plan: before.plan, configSHA256: sha4(before.before) }) + `
|
|
41571
|
+
`);
|
|
41572
|
+
need7(sha4(readPrivate(before.canonicalPath)) === sha4(before.before));
|
|
41573
|
+
const result = await rpc.request("config/batchWrite", {
|
|
41574
|
+
filePath: before.plan.configPath,
|
|
41575
|
+
expectedVersion: before.plan.configVersion,
|
|
41576
|
+
reloadUserConfig: true,
|
|
41577
|
+
edits: [{ keyPath: "hooks.state." + JSON.stringify(before.guard.key), value: before.desired, mergeStrategy: "replace" }]
|
|
41578
|
+
});
|
|
41579
|
+
need7(result?.status === "ok" && result.filePath === before.plan.configPath && result.overriddenMetadata === null && /^sha256:[a-f0-9]{64}$/.test(result.version));
|
|
41580
|
+
const after = await inspect(rpc, home, cwd);
|
|
41581
|
+
expectedLayer.version = result.version;
|
|
41582
|
+
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));
|
|
41583
|
+
const receipt = {
|
|
41584
|
+
ok: true,
|
|
41585
|
+
changed: true,
|
|
41586
|
+
operationId,
|
|
41587
|
+
planDigest: before.plan.planDigest,
|
|
41588
|
+
configVersion: result.version,
|
|
41589
|
+
guardVerified: true,
|
|
41590
|
+
nativeDiscoveryVerified: true,
|
|
41591
|
+
nativeAdoptionVerified: false,
|
|
41592
|
+
unrelatedConfigurationPreserved: true
|
|
41593
|
+
};
|
|
41594
|
+
exclusive(join24(directory2, "receipt.json"), JSON.stringify(receipt) + `
|
|
41595
|
+
`);
|
|
41596
|
+
return receipt;
|
|
41597
|
+
} catch (error61) {
|
|
41598
|
+
if (operationId)
|
|
41599
|
+
throw new CodexTrustError("trust_outcome_uncertain", operationId);
|
|
41600
|
+
throw error61;
|
|
41601
|
+
} finally {
|
|
41602
|
+
await rpc.close();
|
|
41603
|
+
}
|
|
41604
|
+
}
|
|
41605
|
+
// src/lib/claude-safety-check.ts
|
|
41606
|
+
import { constants as constants14, closeSync as closeSync15, fstatSync as fstatSync7, lstatSync as lstatSync21, openSync as openSync15, readFileSync as readFileSync23, realpathSync as realpathSync11 } from "fs";
|
|
41607
|
+
import { createHash as createHash17 } from "crypto";
|
|
41608
|
+
import { dirname as dirname22, isAbsolute as isAbsolute19, join as join27, resolve as resolve18 } from "path";
|
|
41609
|
+
import { homedir as homedir8 } from "os";
|
|
41610
|
+
class ClaudeSafetyError extends Error {
|
|
41611
|
+
code;
|
|
41612
|
+
constructor(code2) {
|
|
41613
|
+
super("Claude safety configuration check failed (" + code2 + ").");
|
|
41614
|
+
this.code = code2;
|
|
41615
|
+
this.name = "ClaudeSafetyError";
|
|
41616
|
+
}
|
|
41617
|
+
}
|
|
41618
|
+
function need8(value, code2) {
|
|
41619
|
+
if (!value)
|
|
41620
|
+
throw new ClaudeSafetyError(code2);
|
|
41621
|
+
}
|
|
41622
|
+
var sha6 = (value) => createHash17("sha256").update(value).digest("hex");
|
|
41623
|
+
function read3(path) {
|
|
41624
|
+
try {
|
|
41625
|
+
const resolved = realpathSync11(path);
|
|
41626
|
+
for (let parent = dirname22(resolved);; parent = dirname22(parent)) {
|
|
41627
|
+
const s = lstatSync21(parent);
|
|
41628
|
+
need8(s.isDirectory() && [0, process.getuid?.()].includes(s.uid) && !(s.mode & 18), "settings_unsafe");
|
|
41629
|
+
if (dirname22(parent) === parent)
|
|
41630
|
+
break;
|
|
41631
|
+
}
|
|
41632
|
+
const fd = openSync15(resolved, constants14.O_RDONLY | constants14.O_NOFOLLOW | constants14.O_NONBLOCK);
|
|
41633
|
+
try {
|
|
41634
|
+
const s = fstatSync7(fd);
|
|
41635
|
+
need8(s.isFile() && [0, process.getuid?.()].includes(s.uid) && !(s.mode & 18) && s.size <= 1048576, "settings_unsafe");
|
|
41636
|
+
const raw = readFileSync23(fd, "utf8");
|
|
41637
|
+
need8(Buffer.byteLength(raw) <= 1048576, "settings_unsafe");
|
|
41638
|
+
return { path, resolved, sha256: sha6(raw), raw };
|
|
41639
|
+
} finally {
|
|
41640
|
+
closeSync15(fd);
|
|
41641
|
+
}
|
|
41642
|
+
} catch (error61) {
|
|
41643
|
+
if (error61.code === "ENOENT")
|
|
41644
|
+
return { path, resolved: null, sha256: null, raw: null };
|
|
41645
|
+
if (error61 instanceof ClaudeSafetyError)
|
|
41646
|
+
throw error61;
|
|
41647
|
+
throw new ClaudeSafetyError("settings_unsafe");
|
|
41648
|
+
}
|
|
41649
|
+
}
|
|
41650
|
+
async function verifyClaudeSafetyConfiguration(options = {}, dependencies = {}) {
|
|
41651
|
+
const home = realpathSync11(options.home ?? homedir8()), cwd = realpathSync11(options.cwd ?? process.cwd());
|
|
41652
|
+
const paths = [
|
|
41653
|
+
options.userSettingsPath ?? join27(home, ".claude/settings.json"),
|
|
41654
|
+
join27(cwd, ".claude/settings.json"),
|
|
41655
|
+
options.localSettingsPath ?? join27(cwd, ".claude/settings.local.json")
|
|
41656
|
+
];
|
|
41657
|
+
need8(paths.every((path) => isAbsolute19(path) && resolve18(path) === path), "settings_unsafe");
|
|
41658
|
+
const inspect2 = () => [...new Set(paths)].map(read3);
|
|
41659
|
+
const before = inspect2(), unique = [...new Map(before.filter((file2) => file2.resolved !== null).map((file2) => [file2.resolved, file2])).values()];
|
|
41660
|
+
const guards = [];
|
|
41661
|
+
for (const file2 of unique) {
|
|
41662
|
+
let settings;
|
|
41663
|
+
try {
|
|
41664
|
+
settings = JSON.parse(file2.raw);
|
|
41665
|
+
} catch {
|
|
41666
|
+
throw new ClaudeSafetyError("settings_invalid");
|
|
41667
|
+
}
|
|
41668
|
+
need8(settings && typeof settings === "object" && !Array.isArray(settings), "settings_invalid");
|
|
41669
|
+
need8(settings.disableAllHooks === undefined || settings.disableAllHooks === false, "hooks_disabled");
|
|
41670
|
+
if (settings.hooks === undefined)
|
|
41671
|
+
continue;
|
|
41672
|
+
need8(settings.hooks && typeof settings.hooks === "object" && !Array.isArray(settings.hooks), "settings_invalid");
|
|
41673
|
+
let count = 0;
|
|
41674
|
+
for (const [event, groups] of Object.entries(settings.hooks)) {
|
|
41675
|
+
need8(Array.isArray(groups), "settings_invalid");
|
|
41676
|
+
for (const group of groups) {
|
|
41677
|
+
need8(group && Array.isArray(group.hooks), "settings_invalid");
|
|
41678
|
+
for (const handler of group.hooks) {
|
|
41679
|
+
need8(++count <= 1e4 && handler && typeof handler === "object", "settings_invalid");
|
|
41680
|
+
if (typeof handler.command !== "string")
|
|
41681
|
+
continue;
|
|
41682
|
+
const binding = nativeSafetyCommandBinding(handler.command);
|
|
41683
|
+
if (/\bhasna-native-safety-v\d+\b/.test(handler.command))
|
|
41684
|
+
need8(binding, "guard_definition_invalid");
|
|
41685
|
+
need8(!/^(?:hooks run trash-guard|hook-trash-guard)(?:\s|$)/.test(handler.command.trim()), "guard_definition_invalid");
|
|
41686
|
+
if (binding?.name !== "trash-guard")
|
|
41687
|
+
continue;
|
|
41688
|
+
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");
|
|
41689
|
+
guards.push({ command: handler.command, path: file2.path });
|
|
41690
|
+
}
|
|
41691
|
+
}
|
|
41692
|
+
}
|
|
41693
|
+
}
|
|
41694
|
+
need8(guards.length !== 0, "guard_missing");
|
|
41695
|
+
need8(guards.length === 1, "guard_ambiguous");
|
|
41696
|
+
try {
|
|
41697
|
+
await (dependencies.verify ?? verifyNativeSafetyCommand)(guards[0].command, home);
|
|
41698
|
+
} catch {
|
|
41699
|
+
throw new ClaudeSafetyError("guard_failed");
|
|
41700
|
+
}
|
|
41701
|
+
need8(JSON.stringify(inspect2()) === JSON.stringify(before), "settings_changed");
|
|
41702
|
+
return {
|
|
41703
|
+
ok: true,
|
|
41704
|
+
target: "claude",
|
|
41705
|
+
cwd,
|
|
41706
|
+
configurationVerified: true,
|
|
41707
|
+
guardVerified: true,
|
|
41708
|
+
sources: before.map(({ path, sha256 }) => ({ path, sha256 })),
|
|
41709
|
+
guardSHA256: sha6(guards[0].command),
|
|
41710
|
+
runtimePolicyVerified: false,
|
|
41711
|
+
nativeDiscoveryVerified: false,
|
|
41712
|
+
nativeAdoptionVerified: false
|
|
41713
|
+
};
|
|
41714
|
+
}
|
|
41715
|
+
// src/lib/native-safety-registration.ts
|
|
41716
|
+
import { createHash as createHash19, randomUUID as randomUUID5 } from "crypto";
|
|
41717
|
+
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";
|
|
41718
|
+
import { basename as basename9, dirname as dirname23, isAbsolute as isAbsolute20, join as join29, relative as relative3, resolve as resolve19 } from "path";
|
|
41719
|
+
import { homedir as homedir9 } from "os";
|
|
41720
|
+
class NativeRegistrationError extends Error {
|
|
41721
|
+
code;
|
|
41722
|
+
operationId;
|
|
41723
|
+
constructor(code2, operationId) {
|
|
41724
|
+
super("Native safety registration failed (" + code2 + ").");
|
|
41725
|
+
this.code = code2;
|
|
41726
|
+
this.operationId = operationId;
|
|
41727
|
+
this.name = "NativeRegistrationError";
|
|
41728
|
+
}
|
|
41729
|
+
}
|
|
41730
|
+
var need9 = (value, code2) => {
|
|
41731
|
+
if (!value)
|
|
41732
|
+
throw new NativeRegistrationError(code2);
|
|
41733
|
+
};
|
|
41734
|
+
var sha7 = (value) => createHash19("sha256").update(value).digest("hex");
|
|
41735
|
+
function canonical6(value) {
|
|
41736
|
+
if (Array.isArray(value))
|
|
41737
|
+
return "[" + value.map(canonical6).join(",") + "]";
|
|
41738
|
+
if (value !== null && typeof value === "object")
|
|
41739
|
+
return "{" + Object.keys(value).sort().map((key) => JSON.stringify(key) + ":" + canonical6(value[key])).join(",") + "}";
|
|
41740
|
+
return JSON.stringify(value);
|
|
41741
|
+
}
|
|
41742
|
+
function safeDirectory3(path) {
|
|
41743
|
+
for (let p = path;; p = dirname23(p)) {
|
|
41744
|
+
const s = lstatSync23(p);
|
|
41745
|
+
need9(s.isDirectory() && [0, process.getuid?.()].includes(s.uid) && !(s.mode & 18), "settings_unsafe");
|
|
41746
|
+
if (dirname23(p) === p)
|
|
41747
|
+
return;
|
|
41748
|
+
}
|
|
41749
|
+
}
|
|
41750
|
+
function read5(path) {
|
|
41751
|
+
let resolved;
|
|
41752
|
+
try {
|
|
41753
|
+
resolved = realpathSync12(path);
|
|
41754
|
+
} catch (error61) {
|
|
41755
|
+
if (error61.code !== "ENOENT")
|
|
41756
|
+
throw new NativeRegistrationError("settings_unsafe");
|
|
41757
|
+
try {
|
|
41758
|
+
lstatSync23(path);
|
|
41759
|
+
throw new NativeRegistrationError("settings_unsafe");
|
|
41760
|
+
} catch (missing2) {
|
|
41761
|
+
if (missing2.code !== "ENOENT")
|
|
41762
|
+
throw missing2;
|
|
41763
|
+
}
|
|
41764
|
+
let ancestor = dirname23(path);
|
|
41765
|
+
const suffix = [basename9(path)];
|
|
41766
|
+
for (;; ) {
|
|
41767
|
+
try {
|
|
41768
|
+
resolved = join29(realpathSync12(ancestor), ...suffix);
|
|
41769
|
+
break;
|
|
41770
|
+
} catch (missing2) {
|
|
41771
|
+
if (missing2.code !== "ENOENT" || dirname23(ancestor) === ancestor || suffix.length >= 64)
|
|
41772
|
+
throw new NativeRegistrationError("settings_unsafe");
|
|
41773
|
+
try {
|
|
41774
|
+
lstatSync23(ancestor);
|
|
41775
|
+
throw new NativeRegistrationError("settings_unsafe");
|
|
41776
|
+
} catch (absent) {
|
|
41777
|
+
if (absent.code !== "ENOENT")
|
|
41778
|
+
throw absent;
|
|
41779
|
+
}
|
|
41780
|
+
suffix.unshift(basename9(ancestor));
|
|
41781
|
+
ancestor = dirname23(ancestor);
|
|
41782
|
+
}
|
|
41783
|
+
}
|
|
41784
|
+
safeDirectory3(realpathSync12(ancestor));
|
|
41785
|
+
return { resolved, raw: null, sha256: "absent" };
|
|
41786
|
+
}
|
|
41787
|
+
safeDirectory3(dirname23(resolved));
|
|
41788
|
+
let fd;
|
|
41789
|
+
try {
|
|
41790
|
+
fd = openSync17(resolved, constants15.O_RDONLY | constants15.O_NOFOLLOW | constants15.O_NONBLOCK);
|
|
41791
|
+
} catch (error61) {
|
|
41792
|
+
if (error61.code === "ENOENT")
|
|
41793
|
+
return { resolved, raw: null, sha256: "absent" };
|
|
41794
|
+
throw new NativeRegistrationError("settings_unsafe");
|
|
41795
|
+
}
|
|
41796
|
+
try {
|
|
41797
|
+
const s = fstatSync15(fd);
|
|
41798
|
+
need9(s.isFile() && s.uid === process.getuid?.() && !(s.mode & 18) && s.nlink === 1 && s.size <= 1048576, "settings_unsafe");
|
|
41799
|
+
const bytes = readFileSync25(fd);
|
|
41800
|
+
need9(bytes.length <= 1048576, "settings_unsafe");
|
|
41801
|
+
const raw = bytes.toString("utf8");
|
|
41802
|
+
need9(Buffer.from(raw).equals(bytes), "settings_invalid");
|
|
41803
|
+
return { resolved, raw, sha256: sha7(bytes) };
|
|
41804
|
+
} finally {
|
|
41805
|
+
closeSync17(fd);
|
|
41806
|
+
}
|
|
41807
|
+
}
|
|
41808
|
+
function assertLosslessJson(raw) {
|
|
41809
|
+
const tokens = /"(?:\\[\s\S]|[^"\\])*"|-?(?:0|[1-9][0-9]*)(?:\.[0-9]+)?(?:[eE][+-]?[0-9]+)?|true|false|null|[{}\[\],:]/g;
|
|
41810
|
+
const stack = [];
|
|
41811
|
+
function decimal(text3) {
|
|
41812
|
+
const match = /^(-?)([0-9]+)(?:\.([0-9]+))?(?:[eE]([+-]?[0-9]+))?$/.exec(text3);
|
|
41813
|
+
need9((match[4]?.replace(/^[+-]/, "").length ?? 0) <= 6, "settings_invalid");
|
|
41814
|
+
let digits = (match[2] + (match[3] ?? "")).replace(/^0+/, "");
|
|
41815
|
+
if (!digits)
|
|
41816
|
+
return match[1] ? "-0" : "0";
|
|
41817
|
+
let exponent = Number(match[4] ?? "0") - (match[3]?.length ?? 0);
|
|
41818
|
+
while (digits.endsWith("0")) {
|
|
41819
|
+
digits = digits.slice(0, -1);
|
|
41820
|
+
exponent++;
|
|
41821
|
+
}
|
|
41822
|
+
return match[1] + digits + "e" + exponent;
|
|
41823
|
+
}
|
|
41824
|
+
for (const match of raw.matchAll(tokens)) {
|
|
41825
|
+
const token = match[0], current2 = stack.at(-1);
|
|
41826
|
+
if (token === "{")
|
|
41827
|
+
stack.push({ kind: "object", key: true, names: new Set });
|
|
41828
|
+
else if (token === "[")
|
|
41829
|
+
stack.push({ kind: "array", key: false, names: new Set });
|
|
41830
|
+
else if (token === "}" || token === "]")
|
|
41831
|
+
stack.pop();
|
|
41832
|
+
else if (token === "," && current2?.kind === "object")
|
|
41833
|
+
current2.key = true;
|
|
41834
|
+
else if (token.startsWith('"') && current2?.kind === "object" && current2.key) {
|
|
41835
|
+
const key = JSON.parse(token);
|
|
41836
|
+
need9(!current2.names.has(key), "settings_invalid");
|
|
41837
|
+
current2.names.add(key);
|
|
41838
|
+
current2.key = false;
|
|
41839
|
+
} else if (/^-?[0-9]/.test(token)) {
|
|
41840
|
+
const parsed = Number(token);
|
|
41841
|
+
need9(Number.isFinite(parsed) && decimal(token) === decimal(JSON.stringify(parsed)), "settings_invalid");
|
|
41842
|
+
}
|
|
41843
|
+
}
|
|
41844
|
+
}
|
|
41845
|
+
function validateSettings(raw) {
|
|
41846
|
+
let settings;
|
|
41847
|
+
try {
|
|
41848
|
+
settings = JSON.parse(raw ?? "{}");
|
|
41849
|
+
} catch {
|
|
41850
|
+
throw new NativeRegistrationError("settings_invalid");
|
|
41851
|
+
}
|
|
41852
|
+
need9(settings && typeof settings === "object" && !Array.isArray(settings), "settings_invalid");
|
|
41853
|
+
assertLosslessJson(raw ?? "{}");
|
|
41854
|
+
need9(settings.disableAllHooks === undefined || settings.disableAllHooks === false, "hooks_disabled");
|
|
41855
|
+
if (settings.hooks !== undefined) {
|
|
41856
|
+
need9(settings.hooks && typeof settings.hooks === "object" && !Array.isArray(settings.hooks), "settings_invalid");
|
|
41857
|
+
for (const groups of Object.values(settings.hooks)) {
|
|
41858
|
+
need9(Array.isArray(groups), "settings_invalid");
|
|
41859
|
+
for (const group of groups) {
|
|
41860
|
+
need9(group && Array.isArray(group.hooks), "settings_invalid");
|
|
41861
|
+
for (const hook of group.hooks) {
|
|
41862
|
+
need9(hook && typeof hook === "object", "settings_invalid");
|
|
41863
|
+
if (registrationName(hook.command) === "trash-guard") {
|
|
41864
|
+
need9((hook.async === undefined || hook.async === false) && hook.asyncRewake !== true, "registration_conflict");
|
|
41865
|
+
}
|
|
41866
|
+
}
|
|
41867
|
+
}
|
|
41868
|
+
}
|
|
41869
|
+
}
|
|
41870
|
+
return settings;
|
|
41871
|
+
}
|
|
41872
|
+
function inspect2(options) {
|
|
41873
|
+
need9(options?.target === "codex" || options?.target === "claude", "input_invalid");
|
|
41874
|
+
const home = realpathSync12(homedir9()), path = getSettingsPath("global", options.target);
|
|
41875
|
+
need9(isAbsolute20(path) && resolve19(path) === path, "input_invalid");
|
|
41876
|
+
const nativeHome = options.target === "codex" ? process.env.CODEX_HOME : process.env.CLAUDE_CONFIG_DIR;
|
|
41877
|
+
if (nativeHome !== undefined) {
|
|
41878
|
+
need9(nativeHome.length > 0 && isAbsolute20(nativeHome), "input_invalid");
|
|
41879
|
+
const selectedNative = read5(resolve19(nativeHome, options.target === "codex" ? "hooks.json" : "settings.json"));
|
|
41880
|
+
need9(read5(path).resolved === selectedNative.resolved, "input_invalid");
|
|
41881
|
+
}
|
|
41882
|
+
const before = read5(path), inside = relative3(home, before.resolved);
|
|
41883
|
+
need9(inside !== "" && !inside.startsWith("..") && !isAbsolute20(inside), "settings_unsafe");
|
|
41884
|
+
const parsed = validateSettings(before.raw);
|
|
41885
|
+
let preview2;
|
|
41886
|
+
try {
|
|
41887
|
+
preview2 = previewNativeSafetyRegistration(options.target);
|
|
41888
|
+
} catch {
|
|
41889
|
+
throw new NativeRegistrationError("registration_conflict");
|
|
41890
|
+
}
|
|
41891
|
+
need9(preview2.before === before.raw, "settings_changed");
|
|
41892
|
+
validateSettings(preview2.after);
|
|
41893
|
+
const changed = canonical6(parsed) !== canonical6(JSON.parse(preview2.after));
|
|
41894
|
+
const base = {
|
|
41895
|
+
schema: "hasna.hooks.native-safety-registration/v1",
|
|
41896
|
+
target: options.target,
|
|
41897
|
+
settingsPath: path,
|
|
41898
|
+
resolvedPath: before.resolved,
|
|
41899
|
+
beforeSHA256: before.sha256,
|
|
41900
|
+
desiredSHA256: changed ? sha7(preview2.after) : before.sha256,
|
|
41901
|
+
commandSHA256: sha7(preview2.command),
|
|
41902
|
+
action: changed ? "register" : "unchanged",
|
|
41903
|
+
guardVerified: true,
|
|
41904
|
+
nativeAdoptionVerified: false
|
|
41905
|
+
};
|
|
41906
|
+
const plan = { ...base, planDigest: sha7(canonical6(base)) };
|
|
41907
|
+
need9(canonical6(read5(path)) === canonical6(before), "settings_changed");
|
|
41908
|
+
return { home, before, preview: preview2, plan };
|
|
41909
|
+
}
|
|
41910
|
+
function syncDirectory2(path) {
|
|
41911
|
+
const fd = openSync17(path, constants15.O_RDONLY | constants15.O_DIRECTORY | constants15.O_NOFOLLOW);
|
|
41912
|
+
try {
|
|
41913
|
+
fsyncSync7(fd);
|
|
41914
|
+
} finally {
|
|
41915
|
+
closeSync17(fd);
|
|
41916
|
+
}
|
|
41917
|
+
}
|
|
41918
|
+
function exclusive2(path, bytes) {
|
|
41919
|
+
const fd = openSync17(path, constants15.O_CREAT | constants15.O_EXCL | constants15.O_WRONLY | constants15.O_NOFOLLOW, 384);
|
|
41920
|
+
try {
|
|
41921
|
+
writeFileSync12(fd, bytes);
|
|
41922
|
+
fsyncSync7(fd);
|
|
41923
|
+
} finally {
|
|
41924
|
+
closeSync17(fd);
|
|
41925
|
+
}
|
|
41926
|
+
syncDirectory2(dirname23(path));
|
|
41927
|
+
}
|
|
41928
|
+
async function planNativeSafetyRegistration(options, dependencies = {}) {
|
|
41929
|
+
const selected = { ...options }, before = inspect2(selected);
|
|
41930
|
+
try {
|
|
41931
|
+
await (dependencies.verify ?? verifyNativeSafetyCommand)(before.preview.command, before.home);
|
|
41932
|
+
} catch {
|
|
41933
|
+
throw new NativeRegistrationError("guard_failed");
|
|
41934
|
+
}
|
|
41935
|
+
need9(inspect2(selected).plan.planDigest === before.plan.planDigest, "settings_changed");
|
|
41936
|
+
return before.plan;
|
|
41937
|
+
}
|
|
41938
|
+
async function applyNativeSafetyRegistration(options, dependencies = {}) {
|
|
41939
|
+
const selected = { ...options };
|
|
41940
|
+
need9(/^[a-f0-9]{64}$/.test(selected.expectedPlanDigest), "input_invalid");
|
|
41941
|
+
const plan = await planNativeSafetyRegistration(selected, dependencies);
|
|
41942
|
+
need9(plan.planDigest === selected.expectedPlanDigest, "settings_changed");
|
|
41943
|
+
const before = inspect2(selected);
|
|
41944
|
+
need9(before.plan.planDigest === plan.planDigest, "settings_changed");
|
|
41945
|
+
if (plan.action === "unchanged")
|
|
41946
|
+
return { ok: true, changed: false, ...plan };
|
|
41947
|
+
const operationId = randomUUID5();
|
|
41948
|
+
let directory2 = before.home;
|
|
41949
|
+
for (const part of [".hasna", "hooks", "native", "trash-registration", operationId]) {
|
|
41950
|
+
directory2 = join29(directory2, part);
|
|
41951
|
+
try {
|
|
41952
|
+
mkdirSync14(directory2, { mode: 448 });
|
|
41953
|
+
syncDirectory2(dirname23(directory2));
|
|
41954
|
+
} catch (error61) {
|
|
41955
|
+
if (error61.code !== "EEXIST")
|
|
41956
|
+
throw new NativeRegistrationError("settings_unsafe");
|
|
41957
|
+
}
|
|
41958
|
+
safeDirectory3(directory2);
|
|
41959
|
+
}
|
|
41960
|
+
if (before.before.raw !== null)
|
|
41961
|
+
exclusive2(join29(directory2, "settings.before"), before.before.raw);
|
|
41962
|
+
exclusive2(join29(directory2, "intent.json"), JSON.stringify({ operationId, plan }) + `
|
|
41963
|
+
`);
|
|
41964
|
+
try {
|
|
41965
|
+
need9(inspect2(selected).plan.planDigest === plan.planDigest, "settings_changed");
|
|
41966
|
+
const result = installHook("trash-guard", { target: selected.target, overwrite: true, expectedSettingsSha256: plan.beforeSHA256 });
|
|
41967
|
+
need9(result.success, "apply_outcome_uncertain");
|
|
41968
|
+
const after = inspect2(selected);
|
|
41969
|
+
need9(after.before.sha256 === plan.desiredSHA256 && after.plan.resolvedPath === plan.resolvedPath && after.plan.action === "unchanged" && after.plan.commandSHA256 === plan.commandSHA256, "apply_outcome_uncertain");
|
|
41970
|
+
const receipt = {
|
|
41971
|
+
ok: true,
|
|
41972
|
+
changed: true,
|
|
41973
|
+
operationId,
|
|
41974
|
+
planDigest: plan.planDigest,
|
|
41975
|
+
settingsSHA256: after.before.sha256,
|
|
41976
|
+
guardVerified: true,
|
|
41977
|
+
nativeAdoptionVerified: false
|
|
41978
|
+
};
|
|
41979
|
+
exclusive2(join29(directory2, "receipt.json"), JSON.stringify(receipt) + `
|
|
41980
|
+
`);
|
|
41981
|
+
return receipt;
|
|
41982
|
+
} catch {
|
|
41983
|
+
throw new NativeRegistrationError("apply_outcome_uncertain", operationId);
|
|
41984
|
+
}
|
|
41985
|
+
}
|
|
40980
41986
|
|
|
40981
41987
|
// src/index.ts
|
|
40982
41988
|
function installHookForProject(name, options = {}) {
|
|
@@ -40999,11 +42005,11 @@ async function runHook(name, input2, options = {}) {
|
|
|
40999
42005
|
throw new Error(`Hook '${name}' not found`);
|
|
41000
42006
|
const hookScript = getHookPath(name) + "/src/hook.ts";
|
|
41001
42007
|
const resolvedScript = custom3?.scriptPath;
|
|
41002
|
-
const script = resolvedScript ?? (
|
|
42008
|
+
const script = resolvedScript ?? (existsSync15(hookScript) ? hookScript : undefined);
|
|
41003
42009
|
if (!script)
|
|
41004
42010
|
throw new Error(`Hook script not found: ${name}`);
|
|
41005
42011
|
const { sha256Of: sha256Of2, checkScriptHash: checkScriptHash2 } = await Promise.resolve().then(() => (init_store(), exports_store));
|
|
41006
|
-
const content =
|
|
42012
|
+
const content = readFileSync26(script);
|
|
41007
42013
|
const check2 = checkScriptHash2(name, sha256Of2(content));
|
|
41008
42014
|
if (!check2.ok) {
|
|
41009
42015
|
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 +42101,7 @@ export {
|
|
|
41095
42101
|
writeLock,
|
|
41096
42102
|
writeCustomHook,
|
|
41097
42103
|
verifyScriptHash,
|
|
42104
|
+
verifyClaudeSafetyConfiguration,
|
|
41098
42105
|
upsertHookRecord,
|
|
41099
42106
|
updateProfile,
|
|
41100
42107
|
uninstallHook,
|
|
@@ -41133,6 +42140,8 @@ export {
|
|
|
41133
42140
|
readCustomManifest,
|
|
41134
42141
|
provisionCloudflareResources,
|
|
41135
42142
|
planSync,
|
|
42143
|
+
planNativeSafetyRegistration,
|
|
42144
|
+
planCodexSafetyTrust,
|
|
41136
42145
|
pinInstalledHook,
|
|
41137
42146
|
parseStorageTables,
|
|
41138
42147
|
parseManifest,
|
|
@@ -41186,12 +42195,15 @@ export {
|
|
|
41186
42195
|
createProfile,
|
|
41187
42196
|
checkScriptHash,
|
|
41188
42197
|
buildCodewithTomlFragment,
|
|
42198
|
+
applyNativeSafetyRegistration,
|
|
42199
|
+
applyCodexSafetyTrust,
|
|
41189
42200
|
allowLocalStore,
|
|
41190
42201
|
STORAGE_TABLES,
|
|
41191
42202
|
STORAGE_DATABASE_ENV,
|
|
41192
42203
|
STORAGE_BACKEND_ENV,
|
|
41193
42204
|
STORAGE_BACKENDS,
|
|
41194
42205
|
RETIRED_STORAGE_MODE_ENV,
|
|
42206
|
+
NativeRegistrationError,
|
|
41195
42207
|
HOOK_EVENTS,
|
|
41196
42208
|
HOOKS_STORAGE_TABLES,
|
|
41197
42209
|
HOOKS_STORAGE_FALLBACK_ENV,
|
|
@@ -41201,5 +42213,7 @@ export {
|
|
|
41201
42213
|
HOOKS_LOCAL_OPT_IN_ENV_KEYS,
|
|
41202
42214
|
HOOKS,
|
|
41203
42215
|
DEFAULT_SERVE_PORT,
|
|
42216
|
+
CodexTrustError,
|
|
42217
|
+
ClaudeSafetyError,
|
|
41204
42218
|
CATEGORIES
|
|
41205
42219
|
};
|