@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
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** The owning Skills writer commits Claude settings and discovery together.
|
|
2
|
+
* A missing policy permits ordinary standalone installation. Any other refusal
|
|
3
|
+
* propagates before settings are written; it never selects a fallback writer.
|
|
4
|
+
*/
|
|
5
|
+
export declare function coordinateClaudeSettings(before: string | null, replacement: string, options: {
|
|
6
|
+
home: string;
|
|
7
|
+
dataDir?: string;
|
|
8
|
+
}): boolean;
|
package/dist/sdk/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// @bun
|
|
2
|
-
//
|
|
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;
|
|
36
|
-
|
|
37
|
-
|
|
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
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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
|
|
|
@@ -28,22 +28,28 @@
|
|
|
28
28
|
*
|
|
29
29
|
* Home spellings (~, $HOME, ${HOME}, including quoted forms) are expanded
|
|
30
30
|
* before classification in Bash targets, file-tool paths, cd operands and
|
|
31
|
-
* apply_patch file markers. Bash relative operands (`.`, `..`, bare names
|
|
32
|
-
* are resolved against the
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
31
|
+
* apply_patch file markers. Bash relative operands (`.`, `..`, bare names,
|
|
32
|
+
* redirection operands) are resolved against the directory the segment runs
|
|
33
|
+
* in: the command's cwd, narrowed by a plain `cd` only across `&&` (see
|
|
34
|
+
* bashTargets). Descriptor duplication (`2>&1`, `>&2`) is not a write.
|
|
35
|
+
* apply_patch tools are inspected through their `*** Add File:` /
|
|
36
|
+
* `*** Update File:` / `*** Delete File:` markers. Parenthesized command
|
|
37
|
+
* groups are unwrapped.
|
|
36
38
|
*
|
|
37
39
|
* Allowed orgs default to hasna,hasnaxyz,hasna-products and are overridable
|
|
38
|
-
* with the WORKSPACE_REPOS_GUARD_ORGS env var (comma-separated)
|
|
39
|
-
*
|
|
40
|
+
* with the WORKSPACE_REPOS_GUARD_ORGS env var (comma-separated) or, where
|
|
41
|
+
* that var cannot reach the guard (the native safety supervisor passes only
|
|
42
|
+
* HOME and PATH), the per-install file
|
|
43
|
+
* $HOME/.hasna/hooks/config/workspace-repos-guard-orgs. Private workspace
|
|
44
|
+
* orgs must be added per-install through one of those; they are never part
|
|
40
45
|
* of the public default.
|
|
41
46
|
* Home is resolved with os.homedir(); never hardcoded. Fail-open on any parse
|
|
42
47
|
* or evaluation error so a guard defect cannot wedge the agent.
|
|
43
48
|
*/
|
|
44
49
|
|
|
50
|
+
import { closeSync, constants, fstatSync, lstatSync, openSync, readFileSync, statSync } from "fs";
|
|
45
51
|
import { homedir } from "os";
|
|
46
|
-
import { isAbsolute, join, normalize, relative, resolve, sep } from "path";
|
|
52
|
+
import { dirname, isAbsolute, join, normalize, relative, resolve, sep } from "path";
|
|
47
53
|
import {
|
|
48
54
|
getCommand,
|
|
49
55
|
readInput,
|
|
@@ -78,15 +84,67 @@ function protectedRoots(home: string): string[] {
|
|
|
78
84
|
return [reposRoot(home), legacyRoot(home)];
|
|
79
85
|
}
|
|
80
86
|
|
|
81
|
-
|
|
87
|
+
/**
|
|
88
|
+
* Per-install allowed-orgs file, read from the guard's own HOME. The native
|
|
89
|
+
* safety supervisor runs this guard with only HOME and PATH, so the
|
|
90
|
+
* WORKSPACE_REPOS_GUARD_ORGS override never reaches it there; this file is the
|
|
91
|
+
* per-install route that does. Org names are separated by commas or
|
|
92
|
+
* whitespace and `#` starts a comment. The file and every directory from it
|
|
93
|
+
* up to HOME must be owned by the user or root and not writable by group or
|
|
94
|
+
* others (the supervisor's own bundle rule). A missing, untrusted, oversized
|
|
95
|
+
* or malformed file is ignored, which leaves the narrower default list.
|
|
96
|
+
*/
|
|
97
|
+
export function allowedOrgsFile(home: string): string {
|
|
98
|
+
return join(home, ".hasna", "hooks", "config", "workspace-repos-guard-orgs");
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const ORG_NAME = /^[A-Za-z0-9](?:[A-Za-z0-9-]{0,37}[A-Za-z0-9])?$/;
|
|
102
|
+
|
|
103
|
+
function ownedAndClosed(stat: { uid: number; mode: number }): boolean {
|
|
104
|
+
const uid = process.getuid?.();
|
|
105
|
+
return uid !== undefined && (stat.uid === 0 || stat.uid === uid) && (stat.mode & 0o022) === 0;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function readAllowedOrgsFile(home: string): Set<string> | null {
|
|
109
|
+
const file = allowedOrgsFile(home);
|
|
110
|
+
let text: string;
|
|
111
|
+
try {
|
|
112
|
+
const fd = openSync(file, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
113
|
+
try {
|
|
114
|
+
const stat = fstatSync(fd);
|
|
115
|
+
if (!stat.isFile() || !ownedAndClosed(stat) || stat.size > 4096) return null;
|
|
116
|
+
text = readFileSync(fd, "utf8");
|
|
117
|
+
} finally {
|
|
118
|
+
closeSync(fd);
|
|
119
|
+
}
|
|
120
|
+
const top = normalize(home);
|
|
121
|
+
for (let dir = dirname(file); dir !== top; dir = dirname(dir)) {
|
|
122
|
+
const stat = lstatSync(dir);
|
|
123
|
+
if (!stat.isDirectory() || !ownedAndClosed(stat) || dir === dirname(dir)) return null;
|
|
124
|
+
}
|
|
125
|
+
if (!ownedAndClosed(statSync(top))) return null;
|
|
126
|
+
} catch {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
const names = text.replace(/#[^\n]*/g, "").split(/[\s,]+/).filter(Boolean);
|
|
130
|
+
return names.length > 0 && names.every((name) => ORG_NAME.test(name)) ? new Set(names) : null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Allowed orgs: the WORKSPACE_REPOS_GUARD_ORGS env var when set, else the
|
|
135
|
+
* per-install file under `home`, else the public default.
|
|
136
|
+
*/
|
|
137
|
+
export function resolveAllowedOrgs(env: NodeJS.ProcessEnv = process.env, home: string = homedir()): Set<string> {
|
|
82
138
|
const raw = env.WORKSPACE_REPOS_GUARD_ORGS;
|
|
83
|
-
if (typeof raw
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
139
|
+
if (typeof raw === "string" && raw.trim()) {
|
|
140
|
+
return new Set(
|
|
141
|
+
raw
|
|
142
|
+
.split(",")
|
|
143
|
+
.map((s) => s.trim())
|
|
144
|
+
.filter(Boolean)
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
return readAllowedOrgsFile(home) ?? new Set(DEFAULT_ORGS);
|
|
90
148
|
}
|
|
91
149
|
|
|
92
150
|
type Operation = "read" | "write" | "delete";
|
|
@@ -139,6 +197,24 @@ export function classifyPath(target: string, root: string, orgs: Set<string>, op
|
|
|
139
197
|
|
|
140
198
|
const WRITE_FLAGS = /\b(?:-o|--output|-O|--output-document|-out)\b/;
|
|
141
199
|
|
|
200
|
+
/**
|
|
201
|
+
* Descriptor duplication or close (`2>&1`, `>&2`, `1>&-`, `0<&3`). It only
|
|
202
|
+
* rewires descriptors that are already open and names no file, so it is
|
|
203
|
+
* never a write.
|
|
204
|
+
*/
|
|
205
|
+
const FD_DUPLICATION = /\d*[<>]&\s*(?:\d+|-)(?=$|[\s;&|)])/g;
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* An output redirection and its file operand: `>`, `>>`, `>|`, `&>`, `&>>`,
|
|
209
|
+
* `<>` and `>&word`, optionally fd-prefixed (`2>`, `2>>`). Apply it only after
|
|
210
|
+
* FD_DUPLICATION is removed, so a remaining `>&word` names a file.
|
|
211
|
+
*/
|
|
212
|
+
const OUTPUT_REDIRECT = /(?:\d+|&)?(?:>>|>\||>&|<>|>)\s*([^\s;&|<>()\x60]*)/g;
|
|
213
|
+
|
|
214
|
+
function withoutFdDuplication(segment: string): string {
|
|
215
|
+
return segment.replace(FD_DUPLICATION, " ");
|
|
216
|
+
}
|
|
217
|
+
|
|
142
218
|
/**
|
|
143
219
|
* Classify the operation of one command segment (a `&&`/`||`/`;`-delimited
|
|
144
220
|
* unit). Git is handled by its subcommand: clean|rm delete, clone|init write,
|
|
@@ -146,7 +222,8 @@ const WRITE_FLAGS = /\b(?:-o|--output|-O|--output-document|-out)\b/;
|
|
|
146
222
|
* in-place flag is a stream filter (redirection is caught separately);
|
|
147
223
|
* rsync/scp/truncate and inline interpreters (python3 -c, node -e, bun -e)
|
|
148
224
|
* are treated as writes — conservative: they only matter when a path under
|
|
149
|
-
* the protected root is also present.
|
|
225
|
+
* the protected root is also present. An output redirection is a write;
|
|
226
|
+
* descriptor duplication (`2>&1`) and `&` are not.
|
|
150
227
|
*/
|
|
151
228
|
function segmentOperation(segment: string): Operation {
|
|
152
229
|
const trimmed = segment.trim();
|
|
@@ -171,7 +248,7 @@ function segmentOperation(segment: string): Operation {
|
|
|
171
248
|
} else if (/(?:^|\s)(?:python3?|node|bun)\b[^;&|]*\s+-[ce](?:\s|$)/.test(trimmed)) {
|
|
172
249
|
return "write";
|
|
173
250
|
}
|
|
174
|
-
if (/(?:^|[;&|])\s*echo\b/.test(trimmed) ||
|
|
251
|
+
if (/(?:^|[;&|])\s*echo\b/.test(trimmed) || />/.test(withoutFdDuplication(trimmed))) return "write";
|
|
175
252
|
return "read";
|
|
176
253
|
}
|
|
177
254
|
|
|
@@ -224,34 +301,79 @@ function regexEscape(text: string): string {
|
|
|
224
301
|
return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
225
302
|
}
|
|
226
303
|
|
|
304
|
+
/**
|
|
305
|
+
* Directories a `cd` operand leads to from each candidate cwd, or null when
|
|
306
|
+
* the operand cannot be resolved statically (`cd -`, variables, globs,
|
|
307
|
+
* command substitution). A bare `cd` goes home.
|
|
308
|
+
*/
|
|
309
|
+
function cdDestinations(operand: string | undefined, bases: string[], home: string): string[] | null {
|
|
310
|
+
if (operand === undefined) return [home];
|
|
311
|
+
let word = expandHomeSpelling(operand, home);
|
|
312
|
+
const quoted = word.match(/^(['"])(.*)\1$/);
|
|
313
|
+
if (quoted) word = quoted[2];
|
|
314
|
+
if (!word || word === "-" || /[$\x60*?[\]{}~\\'"]/.test(word)) return null;
|
|
315
|
+
return [...new Set(bases.map((base) => resolve(base, word)))];
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
const REL_OPERAND = /(?:^|\s)(\.\.?|[^\s"';&|<>()\x60/]+(?:\/[^\s"';&|<>()\x60]*)?)\s*$/;
|
|
319
|
+
|
|
227
320
|
/**
|
|
228
321
|
* Extract protected-repo-checkout targets from a Bash command. Recognises
|
|
229
322
|
* explicit `~/...`, `$HOME/...`, `${HOME}/...` and literal-home references
|
|
230
323
|
* (expanded before classification) under EITHER protected root (the canonical
|
|
231
|
-
* clones root or the legacy workspace/repos root)
|
|
232
|
-
*
|
|
233
|
-
*
|
|
234
|
-
*
|
|
235
|
-
* (
|
|
324
|
+
* clones root or the legacy workspace/repos root). When a writing or deleting
|
|
325
|
+
* segment names no protected path, its relative operands are resolved against
|
|
326
|
+
* every directory the segment can run in, and count only when that directory
|
|
327
|
+
* sits under a protected root: the redirection operands (`> file`), plus the
|
|
328
|
+
* trailing operand (`.`, `..`, a bare name) when the command itself writes or
|
|
329
|
+
* deletes. Tokens outside both protected roots are ignored; reads are
|
|
330
|
+
* returned so callers can decide (they are never blocked).
|
|
331
|
+
*
|
|
332
|
+
* The directory a segment runs in starts as the command's cwd. A plain
|
|
333
|
+
* `cd DIR` narrows it for the segments after it only while they are joined
|
|
334
|
+
* by `&&` from the start of their and-or list, because only then do they run
|
|
335
|
+
* after a successful cd. After `;`, a newline or `||` the cd may have failed,
|
|
336
|
+
* so every directory reached so far stays in scope; a cd inside `( ... )`, a
|
|
337
|
+
* pipeline or the background does not change the outer cwd, and a cd that
|
|
338
|
+
* cannot be resolved statically never narrows it.
|
|
236
339
|
*/
|
|
237
340
|
export function bashTargets(command: string, home: string, cwd: string): PathTarget[] {
|
|
238
341
|
if (!command) return [];
|
|
239
342
|
const roots = protectedRoots(home);
|
|
240
|
-
const
|
|
343
|
+
const underRoot = (path: string) => roots.some((root) => path === root || path.startsWith(`${root}${sep}`));
|
|
344
|
+
const parts = command.split(/(\s*&&\s*|\s*\|\|\s*|;\s*|\n+)/);
|
|
241
345
|
|
|
242
346
|
const targets: PathTarget[] = [];
|
|
243
|
-
|
|
347
|
+
// Every directory the current segment can run in, every directory any
|
|
348
|
+
// segment so far can have left the shell in, and whether the current and-or
|
|
349
|
+
// list has only been joined by `&&` so far.
|
|
350
|
+
let current = [cwd];
|
|
351
|
+
let reachable = new Set([cwd]);
|
|
352
|
+
let certain = true;
|
|
353
|
+
const subshells: Array<{ current: string[]; reachable: Set<string>; certain: boolean }> = [];
|
|
354
|
+
|
|
355
|
+
for (let index = 0; index < parts.length; index += 2) {
|
|
356
|
+
const separator = index === 0 ? "" : parts[index - 1].trim();
|
|
357
|
+
if (separator === "||") certain = false;
|
|
358
|
+
else if (separator !== "&&") certain = true;
|
|
359
|
+
if (separator !== "&&" || !certain) current = [...reachable];
|
|
360
|
+
|
|
361
|
+
const rawSegment = parts[index];
|
|
362
|
+
const opens = (rawSegment.match(/^[\s({]*/)?.[0].match(/\(/g) ?? []).length;
|
|
363
|
+
const closes = (rawSegment.match(/[\s)}]*$/)?.[0].match(/\)/g) ?? []).length;
|
|
364
|
+
for (let i = 0; i < opens; i++) subshells.push({ current: [...current], reachable: new Set(reachable), certain });
|
|
244
365
|
|
|
245
|
-
for (const rawSegment of segments) {
|
|
246
366
|
const segment = unwrapSegment(rawSegment);
|
|
247
367
|
const op = segmentOperation(segment);
|
|
248
368
|
|
|
249
|
-
const
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
369
|
+
const plainCd = segment.match(/^cd(?:\s+(?:-[A-Za-z@]+|--))*(?:\s+([^\s;&|<>()\x60]+))?$/);
|
|
370
|
+
const embeddedCd = plainCd ? null : segment.match(/(?:^|\s)cd(?:\s+(?:-[A-Za-z]+|--))*\s+([^\s;&|<>()\x60]+)/);
|
|
371
|
+
if (embeddedCd) {
|
|
372
|
+
// A cd sharing its segment (pipeline, background, compound command) may
|
|
373
|
+
// or may not have moved the shell: widen, never narrow.
|
|
374
|
+
for (const dir of cdDestinations(embeddedCd[1], current, home) ?? []) reachable.add(dir);
|
|
375
|
+
certain = false;
|
|
376
|
+
current = [...reachable];
|
|
255
377
|
}
|
|
256
378
|
|
|
257
379
|
const homeLiteral = regexEscape(home);
|
|
@@ -267,16 +389,32 @@ export function bashTargets(command: string, home: string, cwd: string): PathTar
|
|
|
267
389
|
}
|
|
268
390
|
|
|
269
391
|
if (!foundExplicit && (op === "delete" || op === "write")) {
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
(
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
392
|
+
const operands: string[] = [];
|
|
393
|
+
const commandPart = withoutFdDuplication(segment).replace(OUTPUT_REDIRECT, (_match, operand: string) => {
|
|
394
|
+
if (operand) operands.push(operand);
|
|
395
|
+
return " ";
|
|
396
|
+
});
|
|
397
|
+
const commandOp = segmentOperation(commandPart);
|
|
398
|
+
const relMatch = commandOp === "read" ? null : commandPart.match(REL_OPERAND);
|
|
399
|
+
for (const base of current.filter(underRoot)) {
|
|
400
|
+
for (const operand of operands) {
|
|
401
|
+
targets.push({ path: normalize(resolve(base, expandHomeSpelling(operand, home))), op: "write" });
|
|
277
402
|
}
|
|
403
|
+
if (relMatch) targets.push({ path: normalize(resolve(base, relMatch[1])), op: commandOp });
|
|
278
404
|
}
|
|
279
405
|
}
|
|
406
|
+
|
|
407
|
+
if (plainCd) {
|
|
408
|
+
const next = cdDestinations(plainCd[1], current, home);
|
|
409
|
+
if (next) {
|
|
410
|
+
for (const dir of next) reachable.add(dir);
|
|
411
|
+
if (certain) current = next;
|
|
412
|
+
} else {
|
|
413
|
+
certain = false;
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
for (let i = 0; i < closes && subshells.length > 0; i++) ({ current, reachable, certain } = subshells.pop()!);
|
|
280
418
|
}
|
|
281
419
|
|
|
282
420
|
return targets;
|
|
@@ -299,14 +437,15 @@ export function patchTargets(patch: string, cwd: string): PathTarget[] {
|
|
|
299
437
|
return targets;
|
|
300
438
|
}
|
|
301
439
|
|
|
302
|
-
|
|
440
|
+
/** `options.home` defaults to os.homedir(); tests pass a private home. */
|
|
441
|
+
export function evaluate(input: CodewithHookInput, options: { home?: string } = {}): { output: CodewithHookOutput; warnings: string[] } {
|
|
303
442
|
const warnings: string[] = [];
|
|
304
443
|
if (input.hook_event_name !== "PreToolUse") return { output: { continue: true }, warnings };
|
|
305
444
|
|
|
306
445
|
const tool = typeof input.tool_name === "string" ? input.tool_name : "";
|
|
307
|
-
const home = homedir();
|
|
446
|
+
const home = options.home ?? homedir();
|
|
308
447
|
const roots = protectedRoots(home);
|
|
309
|
-
const orgs = resolveAllowedOrgs();
|
|
448
|
+
const orgs = resolveAllowedOrgs(process.env, home);
|
|
310
449
|
|
|
311
450
|
if (tool === "Bash") {
|
|
312
451
|
const command = getCommand(input);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hasna/hooks",
|
|
3
|
-
"version": "0.10.
|
|
3
|
+
"version": "0.10.3",
|
|
4
4
|
"description": "Open source hooks library for AI coding agents - Install safety, quality, and automation hooks with a single command",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -60,6 +60,7 @@
|
|
|
60
60
|
"@hasna/contracts": "~1.2.1"
|
|
61
61
|
},
|
|
62
62
|
"dependencies": {
|
|
63
|
+
"@hasna/skills": "0.9.14",
|
|
63
64
|
"@hasna/events": "^0.1.16",
|
|
64
65
|
"@hasna/secrets": "0.4.2",
|
|
65
66
|
"@modelcontextprotocol/sdk": "^1.26.0",
|