@hasna/hooks 0.11.4 → 0.11.5

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/bin/hooks-mcp.js CHANGED
@@ -1419,7 +1419,7 @@ var init_redact = __esm(() => {
1419
1419
  ];
1420
1420
  });
1421
1421
 
1422
- // ../../../../codex0157-source-repair/isolated-build/repo/apps/hooks/node_modules/@hasna/contracts/dist/client/transport.js
1422
+ // ../../node_modules/.bun/@hasna+contracts@1.2.2+944b86d8024e79fa/node_modules/@hasna/contracts/dist/client/transport.js
1423
1423
  import { isIP } from "net";
1424
1424
  import { spawnSync } from "child_process";
1425
1425
  import { closeSync as closeSync22, fstatSync as fstatSync22, openSync as openSync22, readFileSync as readFileSync22 } from "fs";
@@ -3342,7 +3342,7 @@ function writeHookSettingsPreimage(path, settings, before) {
3342
3342
  // src/lib/claude-skills-coordination.ts
3343
3343
  import { createHash as createHash6 } from "crypto";
3344
3344
 
3345
- // ../../../../../../../../.bun/install/cache/@hasna/skills@0.9.17@@@1/dist/index.js
3345
+ // ../../node_modules/.bun/@hasna+skills@0.9.17+a00d3064559565bb/node_modules/@hasna/skills/dist/index.js
3346
3346
  import { existsSync as existsSync10 } from "fs";
3347
3347
  import { homedir as homedir5 } from "os";
3348
3348
  import { join as join9, resolve as resolve3 } from "path";
package/bin/index.js CHANGED
@@ -2520,7 +2520,7 @@ var init_codex_settings = __esm(() => {
2520
2520
  snapshots = new WeakMap;
2521
2521
  });
2522
2522
 
2523
- // ../../../../../../../../.bun/install/cache/@hasna/skills@0.9.17@@@1/dist/index.js
2523
+ // ../../node_modules/.bun/@hasna+skills@0.9.17+a00d3064559565bb/node_modules/@hasna/skills/dist/index.js
2524
2524
  import { existsSync as existsSync10 } from "fs";
2525
2525
  import { homedir as homedir6 } from "os";
2526
2526
  import { join as join10, resolve as resolve4 } from "path";
@@ -35495,7 +35495,7 @@ var init_codex_safety_trust = __esm(() => {
35495
35495
  };
35496
35496
  });
35497
35497
 
35498
- // ../../../../../../../repos/clones/hasna/apps/node_modules/.bun/commander@12.1.0/node_modules/commander/lib/error.js
35498
+ // ../../node_modules/.bun/commander@12.1.0/node_modules/commander/lib/error.js
35499
35499
  var require_error = __commonJS((exports) => {
35500
35500
  class CommanderError extends Error {
35501
35501
  constructor(exitCode, code2, message) {
@@ -35519,7 +35519,7 @@ var require_error = __commonJS((exports) => {
35519
35519
  exports.InvalidArgumentError = InvalidArgumentError;
35520
35520
  });
35521
35521
 
35522
- // ../../../../../../../repos/clones/hasna/apps/node_modules/.bun/commander@12.1.0/node_modules/commander/lib/argument.js
35522
+ // ../../node_modules/.bun/commander@12.1.0/node_modules/commander/lib/argument.js
35523
35523
  var require_argument = __commonJS((exports) => {
35524
35524
  var { InvalidArgumentError } = require_error();
35525
35525
 
@@ -35598,7 +35598,7 @@ var require_argument = __commonJS((exports) => {
35598
35598
  exports.humanReadableArgName = humanReadableArgName;
35599
35599
  });
35600
35600
 
35601
- // ../../../../../../../repos/clones/hasna/apps/node_modules/.bun/commander@12.1.0/node_modules/commander/lib/help.js
35601
+ // ../../node_modules/.bun/commander@12.1.0/node_modules/commander/lib/help.js
35602
35602
  var require_help = __commonJS((exports) => {
35603
35603
  var { humanReadableArgName } = require_argument();
35604
35604
 
@@ -35847,7 +35847,7 @@ var require_help = __commonJS((exports) => {
35847
35847
  exports.Help = Help;
35848
35848
  });
35849
35849
 
35850
- // ../../../../../../../repos/clones/hasna/apps/node_modules/.bun/commander@12.1.0/node_modules/commander/lib/option.js
35850
+ // ../../node_modules/.bun/commander@12.1.0/node_modules/commander/lib/option.js
35851
35851
  var require_option = __commonJS((exports) => {
35852
35852
  var { InvalidArgumentError } = require_error();
35853
35853
 
@@ -35998,7 +35998,7 @@ var require_option = __commonJS((exports) => {
35998
35998
  exports.DualOptions = DualOptions;
35999
35999
  });
36000
36000
 
36001
- // ../../../../../../../repos/clones/hasna/apps/node_modules/.bun/commander@12.1.0/node_modules/commander/lib/suggestSimilar.js
36001
+ // ../../node_modules/.bun/commander@12.1.0/node_modules/commander/lib/suggestSimilar.js
36002
36002
  var require_suggestSimilar = __commonJS((exports) => {
36003
36003
  var maxDistance = 3;
36004
36004
  function editDistance(a, b) {
@@ -36071,7 +36071,7 @@ var require_suggestSimilar = __commonJS((exports) => {
36071
36071
  exports.suggestSimilar = suggestSimilar;
36072
36072
  });
36073
36073
 
36074
- // ../../../../../../../repos/clones/hasna/apps/node_modules/.bun/commander@12.1.0/node_modules/commander/lib/command.js
36074
+ // ../../node_modules/.bun/commander@12.1.0/node_modules/commander/lib/command.js
36075
36075
  var require_command = __commonJS((exports) => {
36076
36076
  var EventEmitter = __require("events").EventEmitter;
36077
36077
  var childProcess = __require("child_process");
@@ -37314,7 +37314,7 @@ Expecting one of '${allowedValues.join("', '")}'`);
37314
37314
  exports.Command = Command;
37315
37315
  });
37316
37316
 
37317
- // ../../../../../../../repos/clones/hasna/apps/node_modules/.bun/commander@12.1.0/node_modules/commander/index.js
37317
+ // ../../node_modules/.bun/commander@12.1.0/node_modules/commander/index.js
37318
37318
  var require_commander = __commonJS((exports) => {
37319
37319
  var { Argument } = require_argument();
37320
37320
  var { Command } = require_command();
@@ -37334,7 +37334,7 @@ var require_commander = __commonJS((exports) => {
37334
37334
  exports.InvalidOptionArgumentError = InvalidArgumentError;
37335
37335
  });
37336
37336
 
37337
- // ../../../../../../../repos/clones/hasna/apps/node_modules/.bun/cli-spinners@2.9.2/node_modules/cli-spinners/spinners.json
37337
+ // ../../node_modules/.bun/cli-spinners@2.9.2/node_modules/cli-spinners/spinners.json
37338
37338
  var require_spinners = __commonJS((exports, module) => {
37339
37339
  module.exports = {
37340
37340
  dots: {
@@ -38960,7 +38960,7 @@ var require_spinners = __commonJS((exports, module) => {
38960
38960
  };
38961
38961
  });
38962
38962
 
38963
- // ../../../../../../../repos/clones/hasna/apps/node_modules/.bun/cli-spinners@2.9.2/node_modules/cli-spinners/index.js
38963
+ // ../../node_modules/.bun/cli-spinners@2.9.2/node_modules/cli-spinners/index.js
38964
38964
  var require_cli_spinners = __commonJS((exports, module) => {
38965
38965
  var spinners = Object.assign({}, require_spinners());
38966
38966
  var spinnersList = Object.keys(spinners);
@@ -39144,7 +39144,7 @@ var init_profiles = __esm(() => {
39144
39144
  init_app_home();
39145
39145
  });
39146
39146
 
39147
- // ../../../../codex0157-source-repair/isolated-build/repo/apps/hooks/node_modules/@hasna/contracts/dist/client/transport.js
39147
+ // ../../node_modules/.bun/@hasna+contracts@1.2.2+944b86d8024e79fa/node_modules/@hasna/contracts/dist/client/transport.js
39148
39148
  import { isIP as isIP2 } from "net";
39149
39149
  import { spawnSync as spawnSync2 } from "child_process";
39150
39150
  import { closeSync as closeSync22, fstatSync as fstatSync22, openSync as openSync22, readFileSync as readFileSync22 } from "fs";
@@ -43633,7 +43633,7 @@ async function verifyClaudeSafetyConfiguration(options = {}, dependencies = {})
43633
43633
  // src/cli/index.tsx
43634
43634
  init_codex_safety_trust();
43635
43635
 
43636
- // ../../../../../../../repos/clones/hasna/apps/apps/events/dist/commander.js
43636
+ // ../events/dist/commander.js
43637
43637
  import { chmod, mkdir, readFile as readFile2, rename, writeFile } from "fs/promises";
43638
43638
  import { Buffer as Buffer2 } from "buffer";
43639
43639
  import { existsSync as existsSync22 } from "fs";
@@ -45289,7 +45289,7 @@ function replaySummary(events2, deliveries, nextCursor) {
45289
45289
  // src/cli/index.tsx
45290
45290
  import { render } from "ink";
45291
45291
 
45292
- // ../../../../../../../repos/clones/hasna/apps/node_modules/.bun/commander@12.1.0/node_modules/commander/esm.mjs
45292
+ // ../../node_modules/.bun/commander@12.1.0/node_modules/commander/esm.mjs
45293
45293
  var import__ = __toESM(require_commander(), 1);
45294
45294
  var {
45295
45295
  program: program2,
@@ -45315,11 +45315,11 @@ import { fileURLToPath as fileURLToPath6 } from "url";
45315
45315
  import { useState as useState7 } from "react";
45316
45316
  import { Box as Box8, Text as Text10, useApp, useInput as useInput5 } from "ink";
45317
45317
 
45318
- // ../../../../../../../repos/clones/hasna/apps/node_modules/.bun/ink-select-input@6.2.0+fa875fef9f949ebd/node_modules/ink-select-input/build/Indicator.js
45318
+ // ../../node_modules/.bun/ink-select-input@6.2.0+fa875fef9f949ebd/node_modules/ink-select-input/build/Indicator.js
45319
45319
  import React from "react";
45320
45320
  import { Box, Text } from "ink";
45321
45321
 
45322
- // ../../../../../../../repos/clones/hasna/apps/node_modules/.bun/is-unicode-supported@2.1.0/node_modules/is-unicode-supported/index.js
45322
+ // ../../node_modules/.bun/is-unicode-supported@2.1.0/node_modules/is-unicode-supported/index.js
45323
45323
  import process3 from "process";
45324
45324
  function isUnicodeSupported() {
45325
45325
  const { env } = process3;
@@ -45330,7 +45330,7 @@ function isUnicodeSupported() {
45330
45330
  return Boolean(env.WT_SESSION) || Boolean(env.TERMINUS_SUBLIME) || env.ConEmuTask === "{cmd::Cmder}" || TERM_PROGRAM === "Terminus-Sublime" || TERM_PROGRAM === "vscode" || TERM === "xterm-256color" || TERM === "alacritty" || TERM === "rxvt-unicode" || TERM === "rxvt-unicode-256color" || env.TERMINAL_EMULATOR === "JetBrains-JediTerm";
45331
45331
  }
45332
45332
 
45333
- // ../../../../../../../repos/clones/hasna/apps/node_modules/.bun/figures@6.1.0/node_modules/figures/index.js
45333
+ // ../../node_modules/.bun/figures@6.1.0/node_modules/figures/index.js
45334
45334
  var common2 = {
45335
45335
  circleQuestionMark: "(?)",
45336
45336
  questionMarkPrefix: "(?)",
@@ -45606,23 +45606,23 @@ var figures = shouldUseMain ? mainSymbols : fallbackSymbols;
45606
45606
  var figures_default = figures;
45607
45607
  var replacements = Object.entries(specialMainSymbols);
45608
45608
 
45609
- // ../../../../../../../repos/clones/hasna/apps/node_modules/.bun/ink-select-input@6.2.0+fa875fef9f949ebd/node_modules/ink-select-input/build/Indicator.js
45609
+ // ../../node_modules/.bun/ink-select-input@6.2.0+fa875fef9f949ebd/node_modules/ink-select-input/build/Indicator.js
45610
45610
  function Indicator({ isSelected = false }) {
45611
45611
  return React.createElement(Box, { marginRight: 1 }, isSelected ? React.createElement(Text, { color: "blue" }, figures_default.pointer) : React.createElement(Text, null, " "));
45612
45612
  }
45613
45613
  var Indicator_default = Indicator;
45614
- // ../../../../../../../repos/clones/hasna/apps/node_modules/.bun/ink-select-input@6.2.0+fa875fef9f949ebd/node_modules/ink-select-input/build/Item.js
45614
+ // ../../node_modules/.bun/ink-select-input@6.2.0+fa875fef9f949ebd/node_modules/ink-select-input/build/Item.js
45615
45615
  import * as React2 from "react";
45616
45616
  import { Text as Text2 } from "ink";
45617
45617
  function Item({ isSelected = false, label }) {
45618
45618
  return React2.createElement(Text2, { color: isSelected ? "blue" : undefined }, label);
45619
45619
  }
45620
45620
  var Item_default = Item;
45621
- // ../../../../../../../repos/clones/hasna/apps/node_modules/.bun/ink-select-input@6.2.0+fa875fef9f949ebd/node_modules/ink-select-input/build/SelectInput.js
45621
+ // ../../node_modules/.bun/ink-select-input@6.2.0+fa875fef9f949ebd/node_modules/ink-select-input/build/SelectInput.js
45622
45622
  import { isDeepStrictEqual } from "util";
45623
45623
  import React3, { useState, useEffect, useRef, useCallback } from "react";
45624
45624
 
45625
- // ../../../../../../../repos/clones/hasna/apps/node_modules/.bun/to-rotated@1.0.0/node_modules/to-rotated/index.js
45625
+ // ../../node_modules/.bun/to-rotated@1.0.0/node_modules/to-rotated/index.js
45626
45626
  function toRotated(array3, steps) {
45627
45627
  if (!Array.isArray(array3)) {
45628
45628
  throw new TypeError(`Expected an array, got \`${typeof array3}\`.`);
@@ -45644,7 +45644,7 @@ function toRotated(array3, steps) {
45644
45644
  ];
45645
45645
  }
45646
45646
 
45647
- // ../../../../../../../repos/clones/hasna/apps/node_modules/.bun/ink-select-input@6.2.0+fa875fef9f949ebd/node_modules/ink-select-input/build/SelectInput.js
45647
+ // ../../node_modules/.bun/ink-select-input@6.2.0+fa875fef9f949ebd/node_modules/ink-select-input/build/SelectInput.js
45648
45648
  import { Box as Box2, useInput } from "ink";
45649
45649
  function SelectInput({ items = [], isFocused = true, initialIndex = 0, indicatorComponent = Indicator_default, itemComponent = Item_default, limit: customLimit, onSelect, onHighlight }) {
45650
45650
  const hasLimit = typeof customLimit === "number" && items.length > customLimit;
@@ -45940,7 +45940,7 @@ function HookSelect({
45940
45940
  import { useState as useState4, useEffect as useEffect3 } from "react";
45941
45941
  import { Box as Box6, Text as Text7, useInput as useInput4 } from "ink";
45942
45942
 
45943
- // ../../../../../../../repos/clones/hasna/apps/node_modules/.bun/ink-text-input@6.0.0+fa875fef9f949ebd/node_modules/ink-text-input/build/index.js
45943
+ // ../../node_modules/.bun/ink-text-input@6.0.0+fa875fef9f949ebd/node_modules/ink-text-input/build/index.js
45944
45944
  import React5, { useState as useState3, useEffect as useEffect2 } from "react";
45945
45945
  import { Text as Text6, useInput as useInput3 } from "ink";
45946
45946
  import chalk from "chalk";
@@ -46118,7 +46118,7 @@ function SearchView({
46118
46118
  import { useState as useState6, useEffect as useEffect5 } from "react";
46119
46119
  import { Box as Box7, Text as Text9 } from "ink";
46120
46120
 
46121
- // ../../../../../../../repos/clones/hasna/apps/node_modules/.bun/ink-spinner@5.0.0+fa875fef9f949ebd/node_modules/ink-spinner/build/index.js
46121
+ // ../../node_modules/.bun/ink-spinner@5.0.0+fa875fef9f949ebd/node_modules/ink-spinner/build/index.js
46122
46122
  var import_cli_spinners = __toESM(require_cli_spinners(), 1);
46123
46123
  import React7, { useState as useState5, useEffect as useEffect4 } from "react";
46124
46124
  import { Text as Text8 } from "ink";
@@ -292,7 +292,7 @@ var REFUSED_VERBS = {
292
292
  unlink: "`unlink` is a delete that trash-guard does not rewrite. Re-run it as `rm -- <path>` and it is redirected into trash.",
293
293
  shred: "`shred` destroys content irrecoverably and is never allowed. Re-run it as `rm -- <path>` and it is redirected into trash."
294
294
  };
295
- var OPAQUE_COMMANDS = new Set(["sh", "bash", "zsh", "dash", "ksh", "ash", "eval", "source", ".", "su", "runuser"]);
295
+ var OPAQUE_COMMANDS = new Set(["sh", "bash", "zsh", "dash", "ksh", "ash", "eval", "source", ".", "su", "runuser", "env"]);
296
296
  var WRAPPERS = {
297
297
  sudo: ["-u", "--user", "-g", "--group", "-p", "--prompt", "-C", "--chdir", "-h", "--host", "-r", "--role", "-t", "--type", "-U", "--other-user"],
298
298
  doas: ["-u", "-C"],
@@ -306,7 +306,7 @@ var WRAPPERS = {
306
306
  setsid: [],
307
307
  command: [],
308
308
  builtin: [],
309
- exec: []
309
+ exec: ["-a"]
310
310
  };
311
311
  var WRAPPER_POSITIONALS = {
312
312
  timeout: 1
@@ -318,6 +318,9 @@ var DELETE_VERB_WORD_RE = /(^|[^A-Za-z0-9_.-])(rm|rmdir|unlink|shred)(?![A-Za-z0
318
318
  function mentionsDeleteVerb(text) {
319
319
  return DELETE_VERB_WORD_RE.test(decodeWord(text));
320
320
  }
321
+ function envSplitCommandText(words) {
322
+ return words.map((word) => word.replace(/^-[iv0]*S|^--split-string=/, " ")).join(" ");
323
+ }
321
324
  var ASSIGNMENT_RE = /^[A-Za-z_][A-Za-z0-9_]*=/;
322
325
  function baseVerb(word) {
323
326
  const slash = word.lastIndexOf("/");
@@ -340,8 +343,8 @@ function lexCommand(command) {
340
343
  if (pendingRedirect)
341
344
  redirects.add(index);
342
345
  wordStart = -1;
346
+ pendingRedirect = false;
343
347
  }
344
- pendingRedirect = false;
345
348
  };
346
349
  const closeSegment = (end) => {
347
350
  closeWord(end);
@@ -349,6 +352,7 @@ function lexCommand(command) {
349
352
  segments.push({ words, redirects });
350
353
  words = [];
351
354
  redirects = new Set;
355
+ pendingRedirect = false;
352
356
  };
353
357
  const scanSingle = (from) => {
354
358
  const end = command.indexOf("'", from + 1);
@@ -495,9 +499,13 @@ function lexCommand(command) {
495
499
  continue;
496
500
  }
497
501
  if (ch === ">" || ch === "<") {
502
+ if (wordStart >= 0 && /^\d+$/.test(command.slice(wordStart, i)))
503
+ wordStart = -1;
498
504
  closeWord(i);
499
505
  pendingRedirect = true;
500
- i += command[i + 1] === ch && ch === ">" ? 2 : 1;
506
+ i++;
507
+ if (command[i] === ch || command[i] === "&" || ch === ">" && command[i] === "|" || ch === "<" && command[i] === ">")
508
+ i++;
501
509
  continue;
502
510
  }
503
511
  if (ch === "'") {
@@ -626,6 +634,10 @@ function commandWordIndex(segment) {
626
634
  let index = 0;
627
635
  let guard = 0;
628
636
  while (index < segment.words.length && guard++ < 32) {
637
+ if (segment.redirects.has(index)) {
638
+ index++;
639
+ continue;
640
+ }
629
641
  const raw = segment.words[index].text;
630
642
  const decoded = decodeWord(raw);
631
643
  if (ASSIGNMENT_RE.test(decoded) && !decoded.startsWith("-")) {
@@ -636,10 +648,25 @@ function commandWordIndex(segment) {
636
648
  index++;
637
649
  continue;
638
650
  }
639
- const wrapperValues = WRAPPERS[decoded];
651
+ const wrapper = baseVerb(decoded);
652
+ if (wrapper === "command") {
653
+ for (const word of segment.words.filter((_, at) => at > index && !segment.redirects.has(at))) {
654
+ const option = decodeWord(word.text);
655
+ if (option === "--" || !option.startsWith("-"))
656
+ break;
657
+ if (/^-[pvV]*[vV][pvV]*$/.test(option))
658
+ return -1;
659
+ }
660
+ }
661
+ const wrapperValues = WRAPPERS[wrapper];
640
662
  if (wrapperValues) {
663
+ const wrapperIndex = index;
641
664
  index++;
642
665
  while (index < segment.words.length) {
666
+ if (segment.redirects.has(index)) {
667
+ index++;
668
+ continue;
669
+ }
643
670
  const option = decodeWord(segment.words[index].text);
644
671
  if (!option.startsWith("-") || option === "-")
645
672
  break;
@@ -647,11 +674,16 @@ function commandWordIndex(segment) {
647
674
  index++;
648
675
  break;
649
676
  }
677
+ if (wrapper === "env" && (/^-[iv0]*S/.test(option) || /^--split-string(?:=|$)/.test(option)))
678
+ return wrapperIndex;
650
679
  index++;
651
- if (wrapperValues.includes(option))
680
+ if (wrapperValues.includes(option) || wrapper === "exec" && /^-[cl]*a$/.test(option)) {
681
+ while (segment.redirects.has(index))
682
+ index++;
652
683
  index++;
684
+ }
653
685
  }
654
- let positionals = WRAPPER_POSITIONALS[decoded] ?? 0;
686
+ let positionals = WRAPPER_POSITIONALS[wrapper] ?? 0;
655
687
  while (positionals > 0 && index < segment.words.length) {
656
688
  const positional = decodeWord(segment.words[index].text);
657
689
  if (positional.startsWith("-") && positional !== "-")
@@ -862,7 +894,8 @@ function scanCommand(command, options) {
862
894
  });
863
895
  continue;
864
896
  }
865
- if (OPAQUE_COMMANDS.has(verb) && mentionsDeleteVerb(segmentText(segment))) {
897
+ const opaqueText = verb === "env" ? envSplitCommandText(segment.words.map((word) => decodeWord(word.text))) : segmentText(segment);
898
+ if (OPAQUE_COMMANDS.has(verb) && mentionsDeleteVerb(opaqueText)) {
866
899
  result.refusals.push({
867
900
  reason: `\`${verb}\` runs a command string this hook cannot rewrite in place. Run the delete directly as \`rm -- <path>\` (intercepted and redirected into trash), or as a script that uses \`trash guard\` itself.`
868
901
  });
@@ -1305,6 +1338,61 @@ function shellWords(command) {
1305
1338
  return words;
1306
1339
  }
1307
1340
  function segmentOperation(segment) {
1341
+ const trimmed = segment.trim();
1342
+ if (!trimmed)
1343
+ return "read";
1344
+ const parsed = lexCommand(trimmed);
1345
+ if (!parsed.trustworthy || parsed.substitutions.length || /\$\{[^}]*[|;()]|[<>]\(/.test(trimmed))
1346
+ return opaqueOperation(trimmed);
1347
+ const operations = parsed.segments.map((part) => {
1348
+ const index = commandWordIndex(part);
1349
+ if (index < 0)
1350
+ return "read";
1351
+ const words = part.words.filter((_, at) => at >= index && !part.redirects.has(at)).map((word) => decodeWord(word.text));
1352
+ const tool = words[0].split("/").pop();
1353
+ if (["rm", "rmdir", "unlink", "shred", "rmtree", "del"].includes(tool ?? ""))
1354
+ return "delete";
1355
+ if (tool === "trash") {
1356
+ let at = 1;
1357
+ while (at < words.length && words[at].startsWith("-")) {
1358
+ const flag = words[at++];
1359
+ if (["--help", "-h", "--version", "-V", "--identity"].includes(flag))
1360
+ return "read";
1361
+ if (["--agent", "--spool", "--files", "--info", "--config"].includes(flag))
1362
+ at++;
1363
+ else if (!["--json", "--local"].includes(flag))
1364
+ return "delete";
1365
+ }
1366
+ if (at === words.length || ["status", "doctor", "list", "info", "get", "pending", "station", "setup", "backup", "storage", "retrieval", "backup-storage", "backup-inspect", "backup-refresh", "help"].includes(words[at]))
1367
+ return "read";
1368
+ if (["restore", "restore-capsule"].includes(words[at]))
1369
+ return "write";
1370
+ return "delete";
1371
+ }
1372
+ if (tool === "git") {
1373
+ const sub = gitSubcommand(part, index);
1374
+ if (sub === "clean" || sub === "rm")
1375
+ return "delete";
1376
+ return sub === "clone" || sub === "init" ? "write" : "read";
1377
+ }
1378
+ if (tool === "env" && mentionsDeleteVerb(envSplitCommandText(words)))
1379
+ return "delete";
1380
+ if (["sh", "bash", "zsh", "dash", "ksh", "ash", "eval", "source", ".", "su", "runuser", "env", "xargs", "find", "busybox", "toybox"].includes(tool ?? "")) {
1381
+ return opaqueOperation(words.join(" "));
1382
+ }
1383
+ if (["mkdir", "mkfile", "touch", "mv", "cp", "ln", "tee", "install", "dd", "rsync", "scp", "truncate"].includes(tool ?? ""))
1384
+ return "write";
1385
+ if (tool === "curl" || tool === "wget")
1386
+ return WRITE_FLAGS.test(words.join(" ")) ? "write" : "read";
1387
+ if (tool === "sed" && words.slice(1).some((word) => /^-i|^--in-place(?:=|$)/.test(word)))
1388
+ return "write";
1389
+ if (["python", "python3", "node", "bun"].includes(tool ?? "") && words.some((word) => word === "-c" || word === "-e"))
1390
+ return "write";
1391
+ return "read";
1392
+ });
1393
+ return operations.includes("delete") ? "delete" : operations.includes("write") ? "write" : "read";
1394
+ }
1395
+ function opaqueOperation(segment) {
1308
1396
  const trimmed = segment.trim();
1309
1397
  if (!trimmed)
1310
1398
  return "read";
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
- // ../../../../../../../repos/clones/hasna/apps/node_modules/.bun/zod@3.25.76/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
- // ../../../../../../../repos/clones/hasna/apps/node_modules/.bun/zod@3.25.76/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
- // ../../../../../../../repos/clones/hasna/apps/node_modules/.bun/zod@3.25.76/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
- // ../../../../../../../repos/clones/hasna/apps/node_modules/.bun/zod@3.25.76/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
- // ../../../../../../../repos/clones/hasna/apps/node_modules/.bun/zod@3.25.76/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
- // ../../../../../../../repos/clones/hasna/apps/node_modules/.bun/zod@3.25.76/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
- // ../../../../../../../repos/clones/hasna/apps/node_modules/.bun/zod@3.25.76/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
- // ../../../../../../../repos/clones/hasna/apps/node_modules/.bun/zod@3.25.76/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
- // ../../../../codex0157-source-repair/isolated-build/repo/apps/hooks/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
- // ../../../../../../../repos/clones/hasna/apps/node_modules/.bun/zod@3.25.76/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
- // ../../../../../../../repos/clones/hasna/apps/node_modules/.bun/zod@3.25.76/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
- // ../../../../../../../repos/clones/hasna/apps/node_modules/.bun/zod@3.25.76/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
- // ../../../../../../../repos/clones/hasna/apps/node_modules/.bun/zod@3.25.76/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
- // ../../../../../../../repos/clones/hasna/apps/node_modules/.bun/zod@3.25.76/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
- // ../../../../../../../repos/clones/hasna/apps/node_modules/.bun/zod@3.25.76/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
- // ../../../../../../../repos/clones/hasna/apps/node_modules/.bun/zod@3.25.76/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
- // ../../../../../../../repos/clones/hasna/apps/node_modules/.bun/zod@3.25.76/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
- // ../../../../../../../repos/clones/hasna/apps/node_modules/.bun/zod@3.25.76/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
- // ../../../../../../../repos/clones/hasna/apps/node_modules/.bun/zod@3.25.76/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
- // ../../../../codex0157-source-repair/isolated-build/repo/apps/hooks/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";
@@ -8106,7 +8106,7 @@ function writeHookSettingsPreimage(path, settings, before) {
8106
8106
  // src/lib/claude-skills-coordination.ts
8107
8107
  import { createHash as createHash6 } from "crypto";
8108
8108
 
8109
- // ../../../../../../../../.bun/install/cache/@hasna/skills@0.9.17@@@1/dist/index.js
8109
+ // ../../node_modules/.bun/@hasna+skills@0.9.17+a00d3064559565bb/node_modules/@hasna/skills/dist/index.js
8110
8110
  import { existsSync as existsSync10 } from "fs";
8111
8111
  import { homedir as homedir5 } from "os";
8112
8112
  import { join as join9, resolve as resolve3 } from "path";
package/dist/sdk/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // @bun
2
- // ../../../../codex0157-source-repair/isolated-build/repo/apps/hooks/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";
@@ -103,7 +103,7 @@ const REFUSED_VERBS: Record<string, string> = {
103
103
  * place. When one of these is in command position and the command mentions a
104
104
  * delete verb, the command is refused rather than guessed at.
105
105
  */
106
- const OPAQUE_COMMANDS = new Set(["sh", "bash", "zsh", "dash", "ksh", "ash", "eval", "source", ".", "su", "runuser"]);
106
+ const OPAQUE_COMMANDS = new Set(["sh", "bash", "zsh", "dash", "ksh", "ash", "eval", "source", ".", "su", "runuser", "env"]);
107
107
 
108
108
  /** Wrapper commands whose real command follows them (with their value-taking options). */
109
109
  const WRAPPERS: Record<string, string[]> = {
@@ -119,7 +119,7 @@ const WRAPPERS: Record<string, string[]> = {
119
119
  setsid: [],
120
120
  command: [],
121
121
  builtin: [],
122
- exec: [],
122
+ exec: ["-a"],
123
123
  };
124
124
 
125
125
  /**
@@ -156,6 +156,12 @@ export function mentionsDeleteVerb(text: string): boolean {
156
156
  return DELETE_VERB_WORD_RE.test(decodeWord(text));
157
157
  }
158
158
 
159
+ /** env's split-string value may be attached to its option. This text is used
160
+ * only to refuse an opaque delete, never as executable text or a rewrite. */
161
+ export function envSplitCommandText(words: string[]): string {
162
+ return words.map((word) => word.replace(/^-[iv0]*S|^--split-string=/, " ")).join(" ");
163
+ }
164
+
159
165
  const ASSIGNMENT_RE = /^[A-Za-z_][A-Za-z0-9_]*=/;
160
166
 
161
167
  /**
@@ -238,8 +244,8 @@ export function lexCommand(command: string): LexResult {
238
244
  words.push({ text: command.slice(wordStart, end), start: wordStart, end });
239
245
  if (pendingRedirect) redirects.add(index);
240
246
  wordStart = -1;
247
+ pendingRedirect = false;
241
248
  }
242
- pendingRedirect = false;
243
249
  };
244
250
 
245
251
  const closeSegment = (end: number): void => {
@@ -247,6 +253,7 @@ export function lexCommand(command: string): LexResult {
247
253
  if (words.length > 0) segments.push({ words, redirects });
248
254
  words = [];
249
255
  redirects = new Set<number>();
256
+ pendingRedirect = false;
250
257
  };
251
258
 
252
259
  const scanSingle = (from: number): number => {
@@ -390,9 +397,13 @@ export function lexCommand(command: string): LexResult {
390
397
  continue;
391
398
  }
392
399
  if (ch === ">" || ch === "<") {
400
+ // An unquoted adjacent descriptor number belongs to the redirect, not
401
+ // the command. Its following operand is also never an executable.
402
+ if (wordStart >= 0 && /^\d+$/.test(command.slice(wordStart, i))) wordStart = -1;
393
403
  closeWord(i);
394
404
  pendingRedirect = true;
395
- i += command[i + 1] === ch && ch === ">" ? 2 : 1;
405
+ i++;
406
+ if (command[i] === ch || command[i] === "&" || (ch === ">" && command[i] === "|") || (ch === "<" && command[i] === ">")) i++;
396
407
  continue;
397
408
  }
398
409
  if (ch === "'") {
@@ -521,10 +532,11 @@ export function decodeWord(text: string): string {
521
532
  * command wrappers — NOT any word that happens to read like a verb, so
522
533
  * `printf '%s\n' rm` is not a delete.
523
534
  */
524
- function commandWordIndex(segment: Segment): number {
535
+ export function commandWordIndex(segment: Segment): number {
525
536
  let index = 0;
526
537
  let guard = 0;
527
538
  while (index < segment.words.length && guard++ < 32) {
539
+ if (segment.redirects.has(index)) { index++; continue; }
528
540
  const raw = segment.words[index].text;
529
541
  const decoded = decodeWord(raw);
530
542
  if (ASSIGNMENT_RE.test(decoded) && !decoded.startsWith("-")) {
@@ -535,20 +547,37 @@ function commandWordIndex(segment: Segment): number {
535
547
  index++;
536
548
  continue;
537
549
  }
538
- const wrapperValues = WRAPPERS[decoded];
550
+ const wrapper = baseVerb(decoded);
551
+ // command -v/-V looks up every following name without executing one.
552
+ if (wrapper === "command") {
553
+ for (const word of segment.words.filter((_, at) => at > index && !segment.redirects.has(at))) {
554
+ const option = decodeWord(word.text);
555
+ if (option === "--" || !option.startsWith("-")) break;
556
+ if (/^-[pvV]*[vV][pvV]*$/.test(option)) return -1;
557
+ }
558
+ }
559
+ const wrapperValues = WRAPPERS[wrapper];
539
560
  if (wrapperValues) {
561
+ const wrapperIndex = index;
540
562
  index++;
541
563
  while (index < segment.words.length) {
564
+ if (segment.redirects.has(index)) { index++; continue; }
542
565
  const option = decodeWord(segment.words[index].text);
543
566
  if (!option.startsWith("-") || option === "-") break;
544
567
  if (option === "--") {
545
568
  index++;
546
569
  break;
547
570
  }
571
+ // env -S reparses its value as a command string. Retain env in
572
+ // command position so the opaque-command refusal owns this form.
573
+ if (wrapper === "env" && (/^-[iv0]*S/.test(option) || /^--split-string(?:=|$)/.test(option))) return wrapperIndex;
548
574
  index++;
549
- if (wrapperValues.includes(option)) index++;
575
+ if (wrapperValues.includes(option) || (wrapper === "exec" && /^-[cl]*a$/.test(option))) {
576
+ while (segment.redirects.has(index)) index++;
577
+ index++;
578
+ }
550
579
  }
551
- let positionals = WRAPPER_POSITIONALS[decoded] ?? 0;
580
+ let positionals = WRAPPER_POSITIONALS[wrapper] ?? 0;
552
581
  while (positionals > 0 && index < segment.words.length) {
553
582
  const positional = decodeWord(segment.words[index].text);
554
583
  if (positional.startsWith("-") && positional !== "-") break;
@@ -814,7 +843,8 @@ export function scanCommand(command: string, options: { cwd: string; home: strin
814
843
  continue;
815
844
  }
816
845
 
817
- if (OPAQUE_COMMANDS.has(verb) && mentionsDeleteVerb(segmentText(segment))) {
846
+ const opaqueText = verb === "env" ? envSplitCommandText(segment.words.map((word) => decodeWord(word.text))) : segmentText(segment);
847
+ if (OPAQUE_COMMANDS.has(verb) && mentionsDeleteVerb(opaqueText)) {
818
848
  result.refusals.push({
819
849
  reason: `\`${verb}\` runs a command string this hook cannot rewrite in place. Run the delete directly as \`rm -- <path>\` (intercepted and redirected into trash), or as a script that uses \`trash guard\` itself.`,
820
850
  });
@@ -849,7 +879,7 @@ function segmentText(segment: Segment): string {
849
879
  }
850
880
 
851
881
  /** The git subcommand, skipping git's own value-taking global options. */
852
- function gitSubcommand(segment: Segment, verbIndex: number): string | null {
882
+ export function gitSubcommand(segment: Segment, verbIndex: number): string | null {
853
883
  let index = verbIndex + 1;
854
884
  while (index < segment.words.length) {
855
885
  const option = decodeWord(segment.words[index].text);
@@ -52,6 +52,7 @@
52
52
  import { closeSync, constants, fstatSync, lstatSync, openSync, readFileSync, statSync } from "fs";
53
53
  import { homedir } from "os";
54
54
  import { dirname, isAbsolute, join, normalize, relative, resolve, sep } from "path";
55
+ import { commandWordIndex, decodeWord, envSplitCommandText, gitSubcommand, lexCommand, mentionsDeleteVerb } from "../../hook-trash-guard/src/hook";
55
56
  import {
56
57
  getCommand,
57
58
  readInput,
@@ -364,6 +365,51 @@ function shellWords(command: string): ShellWord[] {
364
365
  function segmentOperation(segment: string): Operation {
365
366
  const trimmed = segment.trim();
366
367
  if (!trimmed) return "read";
368
+ const parsed = lexCommand(trimmed);
369
+ // Preserve conservative classification where the shell body is opaque.
370
+ if (!parsed.trustworthy || parsed.substitutions.length || /\$\{[^}]*[|;()]|[<>]\(/.test(trimmed)) return opaqueOperation(trimmed);
371
+ const operations = parsed.segments.map((part): Operation => {
372
+ const index = commandWordIndex(part);
373
+ if (index < 0) return "read";
374
+ const words = part.words.filter((_, at) => at >= index && !part.redirects.has(at)).map((word) => decodeWord(word.text));
375
+ const tool = words[0].split("/").pop();
376
+ if (["rm", "rmdir", "unlink", "shred", "rmtree", "del"].includes(tool ?? "")) return "delete";
377
+ if (tool === "trash") {
378
+ // Hasna metadata/setup verbs take identifiers, not paths to remove.
379
+ // Keep legacy positional Trash clients and capture verbs guarded.
380
+ let at = 1;
381
+ while (at < words.length && words[at].startsWith("-")) {
382
+ const flag = words[at++];
383
+ if (["--help", "-h", "--version", "-V", "--identity"].includes(flag)) return "read";
384
+ if (["--agent", "--spool", "--files", "--info", "--config"].includes(flag)) at++;
385
+ else if (!["--json", "--local"].includes(flag)) return "delete";
386
+ }
387
+ if (at === words.length || ["status", "doctor", "list", "info", "get", "pending", "station", "setup", "backup", "storage", "retrieval", "backup-storage", "backup-inspect", "backup-refresh", "help"].includes(words[at])) return "read";
388
+ if (["restore", "restore-capsule"].includes(words[at])) return "write";
389
+ return "delete";
390
+ }
391
+ if (tool === "git") {
392
+ const sub = gitSubcommand(part, index);
393
+ if (sub === "clean" || sub === "rm") return "delete";
394
+ return sub === "clone" || sub === "init" ? "write" : "read";
395
+ }
396
+ if (tool === "env" && mentionsDeleteVerb(envSplitCommandText(words))) return "delete";
397
+ if (["sh", "bash", "zsh", "dash", "ksh", "ash", "eval", "source", ".", "su", "runuser", "env", "xargs", "find", "busybox", "toybox"].includes(tool ?? "")) {
398
+ return opaqueOperation(words.join(" "));
399
+ }
400
+ if (["mkdir", "mkfile", "touch", "mv", "cp", "ln", "tee", "install", "dd", "rsync", "scp", "truncate"].includes(tool ?? "")) return "write";
401
+ if (tool === "curl" || tool === "wget") return WRITE_FLAGS.test(words.join(" ")) ? "write" : "read";
402
+ if (tool === "sed" && words.slice(1).some((word) => /^-i|^--in-place(?:=|$)/.test(word))) return "write";
403
+ if (["python", "python3", "node", "bun"].includes(tool ?? "") && words.some((word) => word === "-c" || word === "-e")) return "write";
404
+ return "read";
405
+ });
406
+ return operations.includes("delete") ? "delete" : operations.includes("write") ? "write" : "read";
407
+ }
408
+
409
+ // Retain the prior conservative behavior for embedded shell bodies.
410
+ function opaqueOperation(segment: string): Operation {
411
+ const trimmed = segment.trim();
412
+ if (!trimmed) return "read";
367
413
 
368
414
  if (/(?:^|\s)\\?(?:[^\s;&|<>()]*\/)?(?:rm|rmdir|unlink|shred|trash|rmtree|del)(?:\s|$)/.test(trimmed)) return "delete";
369
415
  if (/\bgit\b/.test(trimmed)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hasna/hooks",
3
- "version": "0.11.4",
3
+ "version": "0.11.5",
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": {