@hasna/hooks 0.10.2 → 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.
@@ -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 resolveAllowedOrgs(env = process.env) {
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 !== "string" || !raw.trim())
920
- return new Set(DEFAULT_ORGS);
921
- return new Set(raw.split(",").map((s) => s.trim()).filter(Boolean));
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) || /[>&]/.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 segments = command.split(/\s*&&\s*|\s*\|\|\s*|;\s*|\n+/);
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 cwdUnderRepos = null;
1027
- for (const rawSegment of segments) {
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 cdMatch = segment.match(/(?:^|\s)cd(?:\s+(?:-[A-Za-z]+|--))*\s+([^\s;&|<>()\x60]+)/);
1031
- if (cdMatch) {
1032
- const cdTarget = expandHomeSpelling2(cdMatch[1], home);
1033
- if (cdTarget && roots.some((root) => cdTarget === root || cdTarget.startsWith(`${root}${sep2}`))) {
1034
- cwdUnderRepos = cdTarget;
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 base = cwdUnderRepos ?? (roots.some((root) => cwd === root || cwd.startsWith(`${root}${sep2}`)) ? cwd : null);
1050
- if (base) {
1051
- const relMatch = segment.match(/(?:^|\s)(\.\.?|[^\s"';&|<>()\x60/]+(?:\/[^\s"';&|<>()\x60]*)?)\s*$/);
1052
- if (relMatch) {
1053
- targets.push({ path: normalize2(resolve2(base, relMatch[1])), op });
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
@@ -676,7 +676,7 @@ function getHook(name) {
676
676
  import { existsSync as existsSync2, mkdirSync, readFileSync, readdirSync, realpathSync, writeFileSync } from "fs";
677
677
  import { basename, dirname, isAbsolute, join as join3, normalize, resolve as resolve2, sep } from "path";
678
678
 
679
- // ../hooks0102-standalone-lock/node_modules/zod/v3/external.js
679
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/external.js
680
680
  var exports_external = {};
681
681
  __export(exports_external, {
682
682
  void: () => voidType,
@@ -788,7 +788,7 @@ __export(exports_external, {
788
788
  BRAND: () => BRAND
789
789
  });
790
790
 
791
- // ../hooks0102-standalone-lock/node_modules/zod/v3/helpers/util.js
791
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/util.js
792
792
  var util;
793
793
  (function(util2) {
794
794
  util2.assertEqual = (_) => {};
@@ -919,7 +919,7 @@ var getParsedType = (data) => {
919
919
  }
920
920
  };
921
921
 
922
- // ../hooks0102-standalone-lock/node_modules/zod/v3/ZodError.js
922
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/ZodError.js
923
923
  var ZodIssueCode = util.arrayToEnum([
924
924
  "invalid_type",
925
925
  "invalid_literal",
@@ -1038,7 +1038,7 @@ ZodError.create = (issues) => {
1038
1038
  return error;
1039
1039
  };
1040
1040
 
1041
- // ../hooks0102-standalone-lock/node_modules/zod/v3/locales/en.js
1041
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/locales/en.js
1042
1042
  var errorMap = (issue, _ctx) => {
1043
1043
  let message;
1044
1044
  switch (issue.code) {
@@ -1141,7 +1141,7 @@ var errorMap = (issue, _ctx) => {
1141
1141
  };
1142
1142
  var en_default = errorMap;
1143
1143
 
1144
- // ../hooks0102-standalone-lock/node_modules/zod/v3/errors.js
1144
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/errors.js
1145
1145
  var overrideErrorMap = en_default;
1146
1146
  function setErrorMap(map) {
1147
1147
  overrideErrorMap = map;
@@ -1149,7 +1149,7 @@ function setErrorMap(map) {
1149
1149
  function getErrorMap() {
1150
1150
  return overrideErrorMap;
1151
1151
  }
1152
- // ../hooks0102-standalone-lock/node_modules/zod/v3/helpers/parseUtil.js
1152
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/parseUtil.js
1153
1153
  var makeIssue = (params) => {
1154
1154
  const { data, path, errorMaps, issueData } = params;
1155
1155
  const fullPath = [...path, ...issueData.path || []];
@@ -1255,14 +1255,14 @@ var isAborted = (x) => x.status === "aborted";
1255
1255
  var isDirty = (x) => x.status === "dirty";
1256
1256
  var isValid = (x) => x.status === "valid";
1257
1257
  var isAsync = (x) => typeof Promise !== "undefined" && x instanceof Promise;
1258
- // ../hooks0102-standalone-lock/node_modules/zod/v3/helpers/errorUtil.js
1258
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/errorUtil.js
1259
1259
  var errorUtil;
1260
1260
  (function(errorUtil2) {
1261
1261
  errorUtil2.errToObj = (message) => typeof message === "string" ? { message } : message || {};
1262
1262
  errorUtil2.toString = (message) => typeof message === "string" ? message : message?.message;
1263
1263
  })(errorUtil || (errorUtil = {}));
1264
1264
 
1265
- // ../hooks0102-standalone-lock/node_modules/zod/v3/types.js
1265
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/types.js
1266
1266
  class ParseInputLazyPath {
1267
1267
  constructor(parent, value, path, key) {
1268
1268
  this._cachedPath = [];
@@ -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
- // ../hooks0102-standalone-lock/node_modules/@hasna/contracts/dist/client/transport.js
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";
package/dist/index.js CHANGED
@@ -581,7 +581,7 @@ var init_redact = __esm(() => {
581
581
  ];
582
582
  });
583
583
 
584
- // ../hooks0102-standalone-lock/node_modules/zod/v3/helpers/util.js
584
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/util.js
585
585
  var util, objectUtil, ZodParsedType, getParsedType = (data) => {
586
586
  const t = typeof data;
587
587
  switch (t) {
@@ -712,7 +712,7 @@ var init_util = __esm(() => {
712
712
  ]);
713
713
  });
714
714
 
715
- // ../hooks0102-standalone-lock/node_modules/zod/v3/ZodError.js
715
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/ZodError.js
716
716
  var ZodIssueCode, quotelessJson = (obj) => {
717
717
  const json = JSON.stringify(obj, null, 2);
718
718
  return json.replace(/"([^"]+)":/g, "$1:");
@@ -833,7 +833,7 @@ var init_ZodError = __esm(() => {
833
833
  };
834
834
  });
835
835
 
836
- // ../hooks0102-standalone-lock/node_modules/zod/v3/locales/en.js
836
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/locales/en.js
837
837
  var errorMap = (issue, _ctx) => {
838
838
  let message;
839
839
  switch (issue.code) {
@@ -940,7 +940,7 @@ var init_en = __esm(() => {
940
940
  en_default = errorMap;
941
941
  });
942
942
 
943
- // ../hooks0102-standalone-lock/node_modules/zod/v3/errors.js
943
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/errors.js
944
944
  function setErrorMap(map) {
945
945
  overrideErrorMap = map;
946
946
  }
@@ -953,7 +953,7 @@ var init_errors = __esm(() => {
953
953
  overrideErrorMap = en_default;
954
954
  });
955
955
 
956
- // ../hooks0102-standalone-lock/node_modules/zod/v3/helpers/parseUtil.js
956
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/parseUtil.js
957
957
  function addIssueToContext(ctx, issueData) {
958
958
  const overrideMap = getErrorMap();
959
959
  const issue = makeIssue({
@@ -1058,10 +1058,10 @@ var init_parseUtil = __esm(() => {
1058
1058
  });
1059
1059
  });
1060
1060
 
1061
- // ../hooks0102-standalone-lock/node_modules/zod/v3/helpers/typeAliases.js
1061
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/typeAliases.js
1062
1062
  var init_typeAliases = () => {};
1063
1063
 
1064
- // ../hooks0102-standalone-lock/node_modules/zod/v3/helpers/errorUtil.js
1064
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/helpers/errorUtil.js
1065
1065
  var errorUtil;
1066
1066
  var init_errorUtil = __esm(() => {
1067
1067
  (function(errorUtil2) {
@@ -1070,7 +1070,7 @@ var init_errorUtil = __esm(() => {
1070
1070
  })(errorUtil || (errorUtil = {}));
1071
1071
  });
1072
1072
 
1073
- // ../hooks0102-standalone-lock/node_modules/zod/v3/types.js
1073
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/types.js
1074
1074
  class ParseInputLazyPath {
1075
1075
  constructor(parent, value, path, key) {
1076
1076
  this._cachedPath = [];
@@ -4421,7 +4421,7 @@ var init_types = __esm(() => {
4421
4421
  NEVER = INVALID;
4422
4422
  });
4423
4423
 
4424
- // ../hooks0102-standalone-lock/node_modules/zod/v3/external.js
4424
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/v3/external.js
4425
4425
  var exports_external = {};
4426
4426
  __export(exports_external, {
4427
4427
  void: () => voidType,
@@ -4541,7 +4541,7 @@ var init_external = __esm(() => {
4541
4541
  init_ZodError();
4542
4542
  });
4543
4543
 
4544
- // ../hooks0102-standalone-lock/node_modules/zod/index.js
4544
+ // ../../node_modules/.bun/zod@3.25.76/node_modules/zod/index.js
4545
4545
  var init_zod = __esm(() => {
4546
4546
  init_external();
4547
4547
  init_external();
@@ -5101,7 +5101,7 @@ var init_profiles = __esm(() => {
5101
5101
  init_app_home();
5102
5102
  });
5103
5103
 
5104
- // ../hooks0102-standalone-lock/node_modules/@hasna/contracts/dist/client/transport.js
5104
+ // ../../node_modules/.bun/@hasna+contracts@1.2.2+944b86d8024e79fa/node_modules/@hasna/contracts/dist/client/transport.js
5105
5105
  import { isIP } from "net";
5106
5106
  import { spawnSync } from "child_process";
5107
5107
  import { closeSync as closeSync22, fstatSync as fstatSync22, openSync as openSync22, readFileSync as readFileSync22 } from "fs";
@@ -7920,7 +7920,7 @@ function writeCodexSettings(path, settings) {
7920
7920
  // src/lib/claude-skills-coordination.ts
7921
7921
  import { createHash as createHash5 } from "crypto";
7922
7922
 
7923
- // ../hooks0102-standalone-lock/node_modules/@hasna/skills/dist/index.js
7923
+ // ../../node_modules/.bun/@hasna+skills@0.9.14+a00d3064559565bb/node_modules/@hasna/skills/dist/index.js
7924
7924
  import { existsSync as existsSync9 } from "fs";
7925
7925
  import { homedir as homedir4 } from "os";
7926
7926
  import { join as join8, resolve as resolve3 } from "path";
package/dist/sdk/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // @bun
2
- // ../hooks0102-standalone-lock/node_modules/@hasna/contracts/dist/client/transport.js
2
+ // ../../node_modules/.bun/@hasna+contracts@1.2.2+944b86d8024e79fa/node_modules/@hasna/contracts/dist/client/transport.js
3
3
  import { isIP } from "net";
4
4
  import { spawnSync } from "child_process";
5
5
  import { closeSync as closeSync2, fstatSync as fstatSync2, openSync as openSync2, readFileSync as readFileSync2 } from "fs";
@@ -32,17 +32,39 @@ Home spellings (`~`, `$HOME`, `${HOME}`, quoted or not, including split-quote
32
32
  forms like `"$HOME"/.hasna/repos/clones`, which Bash treats identically to the
33
33
  unquoted spelling) are expanded before classification; `apply_patch` tools
34
34
  are inspected through their `Add File` / `Update File` / `Delete File`
35
- markers; Bash relative operands are resolved against the command's cwd when
36
- it sits under the clones root; parenthesized command groups
37
- (`(cd ... && rm -rf ...)`) are unwrapped.
35
+ markers; parenthesized command groups (`(cd ... && rm -rf ...)`) are
36
+ unwrapped.
37
+
38
+ Bash relative operands (a trailing bare name, `.`, `..`, and the operand of
39
+ an output redirection such as `> file` or `>> file`) are resolved against the
40
+ directory the command segment runs in, and only count when that directory is
41
+ under a protected root. That directory starts as the command's cwd. A plain
42
+ `cd DIR` moves it for the segments joined to it by `&&`, so
43
+ `cd /tmp && printf x > out` is not attributed to the session cwd. After `;`,
44
+ a newline or `||` the `cd` may have failed, so the earlier directories stay
45
+ in scope. A `cd` inside `( ... )`, a pipeline or the background does not move
46
+ it, and neither does a `cd` whose target cannot be resolved statically.
47
+ Descriptor duplication (`2>&1`, `>&2`, `1>&-`) is not a write.
38
48
 
39
49
  ## Configuration
40
50
 
41
- Allowed orgs default to `hasna,hasnaxyz,hasna-products` and are overridable
42
- with the `WORKSPACE_REPOS_GUARD_ORGS` env var (comma-separated). Private
43
- workspace orgs must be added per-install via that var; they are never part
44
- of the public default. The home directory is resolved with `os.homedir()` —
45
- never hardcoded.
51
+ Allowed orgs default to `hasna,hasnaxyz,hasna-products`. Private workspace
52
+ orgs must be added per-install; they are never part of the public default.
53
+ There are two per-install routes, checked in this order:
54
+
55
+ 1. The `WORKSPACE_REPOS_GUARD_ORGS` env var (comma-separated), for installs
56
+ that run the hook with the harness environment (`hooks run`).
57
+ 2. The file `$HOME/.hasna/hooks/config/workspace-repos-guard-orgs`. The
58
+ native safety registration (`hooks safety install`) runs the guard with
59
+ only `HOME` and `PATH`, so the env var never reaches it and this file is
60
+ the route that does. List the org names separated by commas or whitespace;
61
+ `#` starts a comment. The list replaces the default, so include the
62
+ default orgs you still need. The file and every directory from it up to
63
+ `HOME` must be owned by the user or root and not writable by group or
64
+ others. A missing, untrusted or malformed file is ignored and the default
65
+ applies.
66
+
67
+ The home directory is resolved with `os.homedir()` — never hardcoded.
46
68
 
47
69
  ## Failure mode
48
70