@hasna/hooks 0.10.1 → 0.10.3
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 +7 -0
- package/bin/hooks-mcp.js +31821 -652
- package/bin/index.js +31834 -860
- package/bin/native-safety-entry.js +115 -23
- package/bin/serve.js +1 -1
- package/dist/index.js +31783 -614
- package/dist/lib/claude-skills-coordination.d.ts +8 -0
- package/dist/sdk/index.js +1 -1
- package/hooks/hook-workspace-repos-guard/README.md +30 -8
- package/hooks/hook-workspace-repos-guard/src/hook.ts +181 -42
- package/package.json +2 -1
|
@@ -899,8 +899,9 @@ function evaluate(input, deps) {
|
|
|
899
899
|
if (false) {}
|
|
900
900
|
|
|
901
901
|
// hooks/hook-workspace-repos-guard/src/hook.ts
|
|
902
|
+
import { closeSync, constants, fstatSync, lstatSync, openSync, readFileSync as readFileSync2, statSync as statSync2 } from "fs";
|
|
902
903
|
import { homedir } from "os";
|
|
903
|
-
import { isAbsolute as isAbsolute2, join as join2, normalize as normalize2, relative, resolve as resolve2, sep as sep2 } from "path";
|
|
904
|
+
import { dirname as dirname2, isAbsolute as isAbsolute2, join as join2, normalize as normalize2, relative, resolve as resolve2, sep as sep2 } from "path";
|
|
904
905
|
var FILE_WRITE_TOOLS = new Set(["Write", "Edit", "MultiEdit", "NotebookEdit"]);
|
|
905
906
|
var PATCH_TOOLS = new Set(["apply_patch", "ApplyPatch", "functions.apply_patch"]);
|
|
906
907
|
var DEFAULT_ORGS = ["hasna", "hasnaxyz", "hasna-products"];
|
|
@@ -914,11 +915,47 @@ function legacyRoot(home) {
|
|
|
914
915
|
function protectedRoots(home) {
|
|
915
916
|
return [reposRoot(home), legacyRoot(home)];
|
|
916
917
|
}
|
|
917
|
-
function
|
|
918
|
+
function allowedOrgsFile(home) {
|
|
919
|
+
return join2(home, ".hasna", "hooks", "config", "workspace-repos-guard-orgs");
|
|
920
|
+
}
|
|
921
|
+
var ORG_NAME = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/;
|
|
922
|
+
function ownedAndClosed(stat) {
|
|
923
|
+
const uid = process.getuid?.();
|
|
924
|
+
return uid !== undefined && (stat.uid === 0 || stat.uid === uid) && (stat.mode & 18) === 0;
|
|
925
|
+
}
|
|
926
|
+
function readAllowedOrgsFile(home) {
|
|
927
|
+
const file = allowedOrgsFile(home);
|
|
928
|
+
let text;
|
|
929
|
+
try {
|
|
930
|
+
const fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
931
|
+
try {
|
|
932
|
+
const stat = fstatSync(fd);
|
|
933
|
+
if (!stat.isFile() || !ownedAndClosed(stat) || stat.size > 4096)
|
|
934
|
+
return null;
|
|
935
|
+
text = readFileSync2(fd, "utf8");
|
|
936
|
+
} finally {
|
|
937
|
+
closeSync(fd);
|
|
938
|
+
}
|
|
939
|
+
const top = normalize2(home);
|
|
940
|
+
for (let dir = dirname2(file);dir !== top; dir = dirname2(dir)) {
|
|
941
|
+
const stat = lstatSync(dir);
|
|
942
|
+
if (!stat.isDirectory() || !ownedAndClosed(stat) || dir === dirname2(dir))
|
|
943
|
+
return null;
|
|
944
|
+
}
|
|
945
|
+
if (!ownedAndClosed(statSync2(top)))
|
|
946
|
+
return null;
|
|
947
|
+
} catch {
|
|
948
|
+
return null;
|
|
949
|
+
}
|
|
950
|
+
const names = text.replace(/#[^\n]*/g, "").split(/[\s,]+/).filter(Boolean);
|
|
951
|
+
return names.length > 0 && names.every((name) => ORG_NAME.test(name)) ? new Set(names) : null;
|
|
952
|
+
}
|
|
953
|
+
function resolveAllowedOrgs(env = process.env, home = homedir()) {
|
|
918
954
|
const raw = env.WORKSPACE_REPOS_GUARD_ORGS;
|
|
919
|
-
if (typeof raw
|
|
920
|
-
return new Set(
|
|
921
|
-
|
|
955
|
+
if (typeof raw === "string" && raw.trim()) {
|
|
956
|
+
return new Set(raw.split(",").map((s) => s.trim()).filter(Boolean));
|
|
957
|
+
}
|
|
958
|
+
return readAllowedOrgsFile(home) ?? new Set(DEFAULT_ORGS);
|
|
922
959
|
}
|
|
923
960
|
function classifyPath(target, root, orgs, op) {
|
|
924
961
|
if (op === "read")
|
|
@@ -952,6 +989,11 @@ function classifyPath(target, root, orgs, op) {
|
|
|
952
989
|
return { blocked: false };
|
|
953
990
|
}
|
|
954
991
|
var WRITE_FLAGS = /\b(?:-o|--output|-O|--output-document|-out)\b/;
|
|
992
|
+
var FD_DUPLICATION = /\d*[<>]&\s*(?:\d+|-)(?=$|[\s;&|)])/g;
|
|
993
|
+
var OUTPUT_REDIRECT = /(?:\d+|&)?(?:>>|>\||>&|<>|>)\s*([^\s;&|<>()\x60]*)/g;
|
|
994
|
+
function withoutFdDuplication(segment) {
|
|
995
|
+
return segment.replace(FD_DUPLICATION, " ");
|
|
996
|
+
}
|
|
955
997
|
function segmentOperation(segment) {
|
|
956
998
|
const trimmed = segment.trim();
|
|
957
999
|
if (!trimmed)
|
|
@@ -980,7 +1022,7 @@ function segmentOperation(segment) {
|
|
|
980
1022
|
} else if (/(?:^|\s)(?:python3?|node|bun)\b[^;&|]*\s+-[ce](?:\s|$)/.test(trimmed)) {
|
|
981
1023
|
return "write";
|
|
982
1024
|
}
|
|
983
|
-
if (/(?:^|[;&|])\s*echo\b/.test(trimmed) ||
|
|
1025
|
+
if (/(?:^|[;&|])\s*echo\b/.test(trimmed) || />/.test(withoutFdDuplication(trimmed)))
|
|
984
1026
|
return "write";
|
|
985
1027
|
return "read";
|
|
986
1028
|
}
|
|
@@ -1017,22 +1059,51 @@ function unwrapSegment(segment) {
|
|
|
1017
1059
|
function regexEscape(text) {
|
|
1018
1060
|
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
1019
1061
|
}
|
|
1062
|
+
function cdDestinations(operand, bases, home) {
|
|
1063
|
+
if (operand === undefined)
|
|
1064
|
+
return [home];
|
|
1065
|
+
let word = expandHomeSpelling2(operand, home);
|
|
1066
|
+
const quoted = word.match(/^(['"])(.*)\1$/);
|
|
1067
|
+
if (quoted)
|
|
1068
|
+
word = quoted[2];
|
|
1069
|
+
if (!word || word === "-" || /[$\x60*?[\]{}~\\'"]/.test(word))
|
|
1070
|
+
return null;
|
|
1071
|
+
return [...new Set(bases.map((base) => resolve2(base, word)))];
|
|
1072
|
+
}
|
|
1073
|
+
var REL_OPERAND = /(?:^|\s)(\.\.?|[^\s"';&|<>()\x60/]+(?:\/[^\s"';&|<>()\x60]*)?)\s*$/;
|
|
1020
1074
|
function bashTargets(command, home, cwd) {
|
|
1021
1075
|
if (!command)
|
|
1022
1076
|
return [];
|
|
1023
1077
|
const roots = protectedRoots(home);
|
|
1024
|
-
const
|
|
1078
|
+
const underRoot = (path) => roots.some((root) => path === root || path.startsWith(`${root}${sep2}`));
|
|
1079
|
+
const parts = command.split(/(\s*&&\s*|\s*\|\|\s*|;\s*|\n+)/);
|
|
1025
1080
|
const targets = [];
|
|
1026
|
-
let
|
|
1027
|
-
|
|
1081
|
+
let current = [cwd];
|
|
1082
|
+
let reachable = new Set([cwd]);
|
|
1083
|
+
let certain = true;
|
|
1084
|
+
const subshells = [];
|
|
1085
|
+
for (let index = 0;index < parts.length; index += 2) {
|
|
1086
|
+
const separator = index === 0 ? "" : parts[index - 1].trim();
|
|
1087
|
+
if (separator === "||")
|
|
1088
|
+
certain = false;
|
|
1089
|
+
else if (separator !== "&&")
|
|
1090
|
+
certain = true;
|
|
1091
|
+
if (separator !== "&&" || !certain)
|
|
1092
|
+
current = [...reachable];
|
|
1093
|
+
const rawSegment = parts[index];
|
|
1094
|
+
const opens = (rawSegment.match(/^[\s({]*/)?.[0].match(/\(/g) ?? []).length;
|
|
1095
|
+
const closes = (rawSegment.match(/[\s)}]*$/)?.[0].match(/\)/g) ?? []).length;
|
|
1096
|
+
for (let i = 0;i < opens; i++)
|
|
1097
|
+
subshells.push({ current: [...current], reachable: new Set(reachable), certain });
|
|
1028
1098
|
const segment = unwrapSegment(rawSegment);
|
|
1029
1099
|
const op = segmentOperation(segment);
|
|
1030
|
-
const
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1100
|
+
const plainCd = segment.match(/^cd(?:\s+(?:-[A-Za-z@]+|--))*(?:\s+([^\s;&|<>()\x60]+))?$/);
|
|
1101
|
+
const embeddedCd = plainCd ? null : segment.match(/(?:^|\s)cd(?:\s+(?:-[A-Za-z]+|--))*\s+([^\s;&|<>()\x60]+)/);
|
|
1102
|
+
if (embeddedCd) {
|
|
1103
|
+
for (const dir of cdDestinations(embeddedCd[1], current, home) ?? [])
|
|
1104
|
+
reachable.add(dir);
|
|
1105
|
+
certain = false;
|
|
1106
|
+
current = [...reachable];
|
|
1036
1107
|
}
|
|
1037
1108
|
const homeLiteral = regexEscape(home);
|
|
1038
1109
|
const suffix = roots.map((root) => regexEscape(root.slice(home.length + 1))).join("|");
|
|
@@ -1046,14 +1117,35 @@ function bashTargets(command, home, cwd) {
|
|
|
1046
1117
|
targets.push({ path: normalize2(expanded).replace(/\/+$/, ""), op });
|
|
1047
1118
|
}
|
|
1048
1119
|
if (!foundExplicit && (op === "delete" || op === "write")) {
|
|
1049
|
-
const
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1120
|
+
const operands = [];
|
|
1121
|
+
const commandPart = withoutFdDuplication(segment).replace(OUTPUT_REDIRECT, (_match, operand) => {
|
|
1122
|
+
if (operand)
|
|
1123
|
+
operands.push(operand);
|
|
1124
|
+
return " ";
|
|
1125
|
+
});
|
|
1126
|
+
const commandOp = segmentOperation(commandPart);
|
|
1127
|
+
const relMatch = commandOp === "read" ? null : commandPart.match(REL_OPERAND);
|
|
1128
|
+
for (const base of current.filter(underRoot)) {
|
|
1129
|
+
for (const operand of operands) {
|
|
1130
|
+
targets.push({ path: normalize2(resolve2(base, expandHomeSpelling2(operand, home))), op: "write" });
|
|
1054
1131
|
}
|
|
1132
|
+
if (relMatch)
|
|
1133
|
+
targets.push({ path: normalize2(resolve2(base, relMatch[1])), op: commandOp });
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
if (plainCd) {
|
|
1137
|
+
const next = cdDestinations(plainCd[1], current, home);
|
|
1138
|
+
if (next) {
|
|
1139
|
+
for (const dir of next)
|
|
1140
|
+
reachable.add(dir);
|
|
1141
|
+
if (certain)
|
|
1142
|
+
current = next;
|
|
1143
|
+
} else {
|
|
1144
|
+
certain = false;
|
|
1055
1145
|
}
|
|
1056
1146
|
}
|
|
1147
|
+
for (let i = 0;i < closes && subshells.length > 0; i++)
|
|
1148
|
+
({ current, reachable, certain } = subshells.pop());
|
|
1057
1149
|
}
|
|
1058
1150
|
return targets;
|
|
1059
1151
|
}
|
|
@@ -1067,14 +1159,14 @@ function patchTargets(patch, cwd) {
|
|
|
1067
1159
|
}
|
|
1068
1160
|
return targets;
|
|
1069
1161
|
}
|
|
1070
|
-
function evaluate2(input) {
|
|
1162
|
+
function evaluate2(input, options = {}) {
|
|
1071
1163
|
const warnings = [];
|
|
1072
1164
|
if (input.hook_event_name !== "PreToolUse")
|
|
1073
1165
|
return { output: { continue: true }, warnings };
|
|
1074
1166
|
const tool = typeof input.tool_name === "string" ? input.tool_name : "";
|
|
1075
|
-
const home = homedir();
|
|
1167
|
+
const home = options.home ?? homedir();
|
|
1076
1168
|
const roots = protectedRoots(home);
|
|
1077
|
-
const orgs = resolveAllowedOrgs();
|
|
1169
|
+
const orgs = resolveAllowedOrgs(process.env, home);
|
|
1078
1170
|
if (tool === "Bash") {
|
|
1079
1171
|
const command = getCommand(input);
|
|
1080
1172
|
if (!command)
|
package/bin/serve.js
CHANGED
|
@@ -5362,7 +5362,7 @@ import { readFileSync as readFileSync5 } from "fs";
|
|
|
5362
5362
|
import { dirname as dirname4, join as join8 } from "path";
|
|
5363
5363
|
import { fileURLToPath as fileURLToPath5 } from "url";
|
|
5364
5364
|
|
|
5365
|
-
//
|
|
5365
|
+
// ../../node_modules/.bun/@hasna+contracts@1.2.2+944b86d8024e79fa/node_modules/@hasna/contracts/dist/client/transport.js
|
|
5366
5366
|
import { spawnSync } from "child_process";
|
|
5367
5367
|
import { closeSync as closeSync2, fstatSync as fstatSync2, openSync as openSync2, readFileSync as readFileSync22 } from "fs";
|
|
5368
5368
|
import { O_NOFOLLOW as O_NOFOLLOW2, O_NONBLOCK as O_NONBLOCK2, O_RDONLY as O_RDONLY2 } from "constants";
|