@wrongstack/plugins 0.300.0 → 0.301.0

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/dist/index.js CHANGED
@@ -4303,6 +4303,9 @@ function positionToLineCol(text, pos) {
4303
4303
  const col = pos - upTo.lastIndexOf("\n");
4304
4304
  return { line, col };
4305
4305
  }
4306
+ function redactParseSnippet(message) {
4307
+ return message.replace(/"[\s\S]{0,200}?"(?= is not valid JSON)/g, '"\u2026"');
4308
+ }
4306
4309
  function validateJson(text, isJsonc, fileName) {
4307
4310
  const source = isJsonc ? stripJsonc(text) : text;
4308
4311
  try {
@@ -4336,10 +4339,10 @@ function validateJson(text, isJsonc, fileName) {
4336
4339
  const idx = source.indexOf(snippetMatch[1]);
4337
4340
  if (idx >= 0) {
4338
4341
  const { line } = positionToLineCol(source, idx);
4339
- return [`JSON parse error near line ${line}: ${message.split("\n")[0]}`];
4342
+ return [`JSON parse error near line ${line}`];
4340
4343
  }
4341
4344
  }
4342
- return [`JSON parse error: ${message.split("\n")[0]}`];
4345
+ return [`JSON parse error: ${redactParseSnippet(message.split("\n")[0] ?? "")}`];
4343
4346
  }
4344
4347
  }
4345
4348
  function validateYaml(text) {
@@ -4476,10 +4479,12 @@ var plugin12 = {
4476
4479
  const cfg = readConfig11(api.config.extensions?.["config-validator"]);
4477
4480
  const hook = (input) => {
4478
4481
  if (!cfg.enabled) return;
4482
+ if (input.toolResult?.isError) return;
4479
4483
  state12.invocations += 1;
4480
4484
  const ti = input.toolInput ?? {};
4481
4485
  const raw = ti["path"] ?? ti["file_path"] ?? ti["filePath"];
4482
4486
  if (typeof raw !== "string" || raw.length === 0) return;
4487
+ if (!withinProject(raw)) return;
4483
4488
  const lower = raw.toLowerCase();
4484
4489
  if (!cfg.extensions.some((ext) => lower.endsWith(ext))) return;
4485
4490
  let text;
@@ -13166,11 +13171,15 @@ function compilePathGlob(pattern) {
13166
13171
  let source = "";
13167
13172
  for (let i = 0; i < normalized.length; i++) {
13168
13173
  const ch = normalized[i];
13174
+ if (ch === "/" && normalized.slice(i) === "/**") {
13175
+ source += "(?:/(?:[^/]+(?:/[^/]+)*)?)?";
13176
+ break;
13177
+ }
13169
13178
  if (ch === "*") {
13170
13179
  if (normalized[i + 1] === "*") {
13171
- source += "(?:.*)";
13172
- i += 1;
13173
- if (normalized[i + 1] === "/") i += 1;
13180
+ const followedBySlash = normalized[i + 2] === "/";
13181
+ source += followedBySlash ? "(?:[^/]+/)*" : "(?:[^/]+(?:/[^/]+)*)?";
13182
+ i += followedBySlash ? 2 : 1;
13174
13183
  } else {
13175
13184
  source += "[^/]*";
13176
13185
  }
@@ -13185,31 +13194,1185 @@ function compilePathGlob(pattern) {
13185
13194
  return new RegExp(`(?:^|/)${source}$`, "i");
13186
13195
  }
13187
13196
  function normalizePath2(p) {
13188
- return p.replace(/\\/g, "/").replace(/^\.\//, "");
13197
+ const slashNormalized = p.replace(/\\/g, "/");
13198
+ const drive = /^[a-z]:/i.exec(slashNormalized)?.[0] ?? "";
13199
+ const absolute = slashNormalized.startsWith("/") || drive.length > 0;
13200
+ const body = drive ? slashNormalized.slice(drive.length).replace(/^\//, "") : slashNormalized;
13201
+ const segments = [];
13202
+ for (const segment of body.split("/")) {
13203
+ if (!segment || segment === ".") continue;
13204
+ if (segment === "..") {
13205
+ if (segments.length > 0 && segments.at(-1) !== "..") {
13206
+ segments.pop();
13207
+ } else if (!absolute) {
13208
+ segments.push(segment);
13209
+ }
13210
+ continue;
13211
+ }
13212
+ segments.push(segment);
13213
+ }
13214
+ const joined = segments.join("/");
13215
+ if (drive) return `${drive}/${joined}`.replace(/\/$/, "");
13216
+ if (slashNormalized.startsWith("/")) return `/${joined}`.replace(/\/$/, "") || "/";
13217
+ return joined;
13218
+ }
13219
+ function isAbsolutePath(path) {
13220
+ return /^(?:\/|[a-z]:\/)/i.test(path);
13221
+ }
13222
+ function resolveTargetPath(path, base) {
13223
+ const normalized = normalizePath2(path);
13224
+ const target = normalized === "" && /^\.\/?$/.test(path.trim()) ? "." : normalized;
13225
+ return base && !isAbsolutePath(target) ? normalizePath2(`${base}/${target}`) : target;
13226
+ }
13227
+ function relativeToInvocationCwd(path, invocationCwd) {
13228
+ const normalizedPath = normalizePath2(path).replace(/\/$/, "");
13229
+ const normalized = normalizedPath === "" && /^\.\/?$/.test(path.trim()) ? "." : normalizedPath;
13230
+ if (!invocationCwd || !isAbsolutePath(normalizePath2(invocationCwd))) return normalized;
13231
+ const root = normalizePath2(invocationCwd).replace(/\/$/, "");
13232
+ const pathForComparison = /^[a-z]:\//i.test(normalized) ? normalized.toLowerCase() : normalized;
13233
+ const rootForComparison = /^[a-z]:\//i.test(root) ? root.toLowerCase() : root;
13234
+ if (pathForComparison === rootForComparison) return ".";
13235
+ if (pathForComparison.startsWith(`${rootForComparison}/`)) {
13236
+ return normalized.slice(root.length + 1);
13237
+ }
13238
+ return normalized;
13239
+ }
13240
+ function effectiveToolCwd(toolInputCwd, invocationCwd) {
13241
+ if (typeof toolInputCwd !== "string" || toolInputCwd.length === 0) return invocationCwd;
13242
+ if (!invocationCwd || isAbsolutePath(normalizePath2(toolInputCwd))) return toolInputCwd;
13243
+ return resolveTargetPath(toolInputCwd, invocationCwd);
13189
13244
  }
13190
13245
  function matchesAny(path, patterns) {
13191
13246
  const normalized = normalizePath2(path);
13192
13247
  return patterns.some((re) => re.test(normalized));
13193
13248
  }
13249
+ function isUnresolvedPathScope(path) {
13250
+ return /[*?]/.test(path);
13251
+ }
13252
+ function isRootPathScope(path) {
13253
+ const normalized = normalizePath2(path).replace(/\/$/, "");
13254
+ return normalized === "." || normalized === "";
13255
+ }
13256
+ function isDirectoryAmbiguousPath(path) {
13257
+ const normalized = normalizePath2(path).replace(/\/$/, "");
13258
+ if (isRootPathScope(normalized)) return true;
13259
+ const basename7 = normalized.slice(normalized.lastIndexOf("/") + 1);
13260
+ return path.endsWith("/") || basename7.length > 0 && !basename7.includes(".");
13261
+ }
13262
+ function hasConfiguredProtectedDescendant(path, patterns) {
13263
+ const normalized = normalizePath2(path).replace(/\/$/, "").toLowerCase();
13264
+ if (!normalized || isUnresolvedPathScope(normalized)) return false;
13265
+ return patterns.some((pattern) => {
13266
+ const prefix = staticPrefix(normalizePath2(pattern)).toLowerCase();
13267
+ return prefix.startsWith(`${normalized}/`);
13268
+ });
13269
+ }
13270
+ function staticPrefix(pattern) {
13271
+ const normalized = normalizePath2(pattern);
13272
+ if (normalized === ".") return "";
13273
+ const wildcardIndex = normalized.search(/[*?]/);
13274
+ return (wildcardIndex === -1 ? normalized : normalized.slice(0, wildcardIndex)).replace(
13275
+ /\/$/,
13276
+ ""
13277
+ );
13278
+ }
13279
+ function hasPartialSegmentWildcard(pattern) {
13280
+ const normalized = normalizePath2(pattern);
13281
+ const wildcardIndex = normalized.search(/[*?]/);
13282
+ return wildcardIndex > 0 && normalized[wildcardIndex - 1] !== "/";
13283
+ }
13284
+ function globWitness(pattern) {
13285
+ return pattern.replace(/\*+/g, "").replace(/\?/g, "x");
13286
+ }
13287
+ function scopesMayOverlap(left, right) {
13288
+ const normalizedRight = normalizePath2(right);
13289
+ if (!normalizedRight.includes("/")) {
13290
+ const leftPrefix2 = staticPrefix(left);
13291
+ const candidate = leftPrefix2 ? `${leftPrefix2}/${globWitness(normalizedRight)}` : globWitness(normalizedRight);
13292
+ if (compilePathGlob(left).test(candidate)) return true;
13293
+ return hasPartialSegmentWildcard(left);
13294
+ }
13295
+ const leftPrefix = staticPrefix(left).toLowerCase();
13296
+ const rightPrefix = staticPrefix(right).toLowerCase();
13297
+ if (!leftPrefix || !rightPrefix) return true;
13298
+ if (leftPrefix === rightPrefix || leftPrefix.startsWith(`${rightPrefix}/`) || rightPrefix.startsWith(`${leftPrefix}/`)) {
13299
+ return true;
13300
+ }
13301
+ return hasPartialSegmentWildcard(left) && rightPrefix.startsWith(leftPrefix) || hasPartialSegmentWildcard(right) && leftPrefix.startsWith(rightPrefix);
13302
+ }
13303
+ function targetIntersectsPatterns(target, patternTexts, patterns) {
13304
+ if (matchesAny(target.path, patterns)) return true;
13305
+ if (target.kind === "file") return false;
13306
+ const normalized = normalizePath2(target.path).replace(/\/$/, "");
13307
+ if (!isUnresolvedPathScope(normalized)) {
13308
+ if (normalized === "." || normalized === "") return patternTexts.length > 0;
13309
+ const descendantScope = `${normalized}/**`;
13310
+ return patternTexts.some(
13311
+ (pattern) => scopesMayOverlap(descendantScope, normalizePath2(pattern))
13312
+ );
13313
+ }
13314
+ return patternTexts.some((pattern) => scopesMayOverlap(normalized, normalizePath2(pattern)));
13315
+ }
13316
+ function targetFullyAllowed(target, allowTexts, allowRes) {
13317
+ if (target.kind === "file") return matchesAny(target.path, allowRes);
13318
+ const normalized = normalizePath2(target.path).replace(/\/$/, "");
13319
+ if (isUnresolvedPathScope(normalized)) {
13320
+ return allowTexts.some((allow, index) => {
13321
+ const allowNormalized = normalizePath2(allow);
13322
+ if (allowNormalized === normalized) return true;
13323
+ const targetPrefix = staticPrefix(normalized);
13324
+ const allowRe = allowRes[index];
13325
+ return targetPrefix.length > 0 && !hasPartialSegmentWildcard(normalized) && allowNormalized.endsWith("/**") && allowRe?.test(targetPrefix);
13326
+ });
13327
+ }
13328
+ return allowTexts.some((allow, index) => {
13329
+ const allowNormalized = normalizePath2(allow);
13330
+ const allowRe = allowRes[index];
13331
+ return allowNormalized.endsWith("/**") && allowRe?.test(`${normalized}/.path-guard-probe`);
13332
+ });
13333
+ }
13334
+ var WRITE_CAPABILITIES = /* @__PURE__ */ new Set(["fs.write", "fs.write.outside-project"]);
13335
+ var DISK_MUTATING_CAPABILITIES = /* @__PURE__ */ new Set(["package.install"]);
13336
+ var SHELL_CAPABILITIES = /* @__PURE__ */ new Set(["shell.arbitrary", "shell.restricted", "shell.exec"]);
13337
+ var LEGACY_SHELL_TOOLS = /* @__PURE__ */ new Set(["bash", "shell", "exec"]);
13338
+ var LEGACY_WRITE_TOOLS = /* @__PURE__ */ new Set([
13339
+ "write",
13340
+ "edit",
13341
+ "patch",
13342
+ "scaffold",
13343
+ "format",
13344
+ "replace",
13345
+ "design",
13346
+ "install"
13347
+ ]);
13348
+ function writesToDisk(input) {
13349
+ const capabilities = input.toolCapabilities;
13350
+ const capabilitiesUndeclared = !capabilities || capabilities.length === 0;
13351
+ const hasWriteCap = capabilities?.some((capability) => WRITE_CAPABILITIES.has(capability)) ?? false;
13352
+ const hasDiskMutatingCap = capabilities?.some((capability) => DISK_MUTATING_CAPABILITIES.has(capability)) ?? false;
13353
+ const hasCommandString = typeof input.toolInput?.["command"] === "string";
13354
+ if (hasCommandString && executesShell(input) && !hasWriteCap && !hasDiskMutatingCap && !(capabilitiesUndeclared && LEGACY_WRITE_TOOLS.has(input.toolName ?? "")))
13355
+ return false;
13356
+ if (!capabilitiesUndeclared) {
13357
+ if (hasWriteCap || hasDiskMutatingCap) return true;
13358
+ if (capabilities.includes("mcp.proxy")) {
13359
+ const segmentedName = (input.toolName ?? "").replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[^a-z0-9]+/gi, "_").toLowerCase();
13360
+ const filesystemWriter = /(?:^|_)filesystem(?:_|$)/.test(segmentedName) && /(?:^|_)(?:add|append|copy|create|delete|edit|mkdir|move|remove|rename|write)(?:_|$)/.test(
13361
+ segmentedName
13362
+ );
13363
+ const toolInput = input.toolInput ?? {};
13364
+ const hasKnownPath = [...PATH_FIELDS, ...PATH_LIST_FIELDS].some(
13365
+ (field) => toolInput[field] !== void 0
13366
+ );
13367
+ if (filesystemWriter && hasKnownPath) return true;
13368
+ }
13369
+ return false;
13370
+ }
13371
+ if (input.toolMutating !== void 0) return input.toolMutating;
13372
+ return LEGACY_WRITE_TOOLS.has(input.toolName ?? "");
13373
+ }
13374
+ function executesShell(input) {
13375
+ const toolName = input.toolName ?? "";
13376
+ if (LEGACY_SHELL_TOOLS.has(toolName)) return true;
13377
+ if (toolName === "git") return false;
13378
+ const segmentedName = toolName.replace(/([a-z0-9])([A-Z])/g, "$1_$2");
13379
+ const isCommandStringRunner = /(?:^|[_.-])(?:command|exec|execute|shell|terminal)(?:$|[_.-])/i.test(segmentedName);
13380
+ const hasShellCapability = input.toolCapabilities?.some((capability) => SHELL_CAPABILITIES.has(capability)) ?? false;
13381
+ const hasCommandString = typeof input.toolInput?.["command"] === "string";
13382
+ return isCommandStringRunner && hasShellCapability || hasCommandString && input.toolMutating === true;
13383
+ }
13384
+ var PATH_FIELDS = [
13385
+ "path",
13386
+ "file_path",
13387
+ "filePath",
13388
+ "target",
13389
+ "target_path",
13390
+ "targetPath",
13391
+ "destination",
13392
+ "dest",
13393
+ "output_path",
13394
+ "outputPath",
13395
+ "out",
13396
+ "file"
13397
+ ];
13398
+ var PATH_LIST_FIELDS = ["files", "paths"];
13399
+ var SOURCE_PATH_FIELDS = [
13400
+ "source",
13401
+ "from",
13402
+ "src",
13403
+ "input_path",
13404
+ "inputPath",
13405
+ "old_path",
13406
+ "oldPath"
13407
+ ];
13408
+ var MOVE_TOOL_NAME = /(?:^|[_.-])(move|rename)(?:$|[_.-])/i;
13409
+ var DIRECTORY_DELETE_TOOL_NAME = /(?:^|[_.-])(?:rmdir|(?:delete|remove)[_.-](?:dir|directory))(?:$|[_.-])/i;
13410
+ function segmentedToolName(toolName) {
13411
+ return toolName.replace(/([a-z0-9])([A-Z])/g, "$1_$2");
13412
+ }
13413
+ function isMoveToolName(toolName) {
13414
+ return MOVE_TOOL_NAME.test(segmentedToolName(toolName));
13415
+ }
13416
+ function isDirectoryDeleteToolName(toolName) {
13417
+ return DIRECTORY_DELETE_TOOL_NAME.test(segmentedToolName(toolName));
13418
+ }
13419
+ function cleanPatchPath(raw) {
13420
+ const trimmed = raw.trim();
13421
+ if (!trimmed || trimmed === "/dev/null") return null;
13422
+ const quoted = /^"([^"]+)"(?:\s|$)/.exec(trimmed)?.[1];
13423
+ const withoutTimestamp = quoted ?? trimmed.replace(/(?:\t|\s+)\d{4}-\d{2}-\d{2}(?:\s.*)?$/, "").trim();
13424
+ if (!withoutTimestamp || withoutTimestamp === "/dev/null") return null;
13425
+ return withoutTimestamp.replace(/^[ab]\//, "");
13426
+ }
13427
+ function pathsFromPatch(patch) {
13428
+ const paths = [];
13429
+ const lines = patch.split(/\r?\n/);
13430
+ for (let index = 0; index + 2 < lines.length; index += 1) {
13431
+ const oldHeader = lines[index];
13432
+ const newHeader = lines[index + 1];
13433
+ const hunkHeader = lines[index + 2];
13434
+ if (!oldHeader?.startsWith("--- ") || !newHeader?.startsWith("+++ ")) continue;
13435
+ if (!hunkHeader?.startsWith("@@ ")) continue;
13436
+ const oldPath = cleanPatchPath(oldHeader.slice(4));
13437
+ const newPath = cleanPatchPath(newHeader.slice(4));
13438
+ if (oldPath) paths.push(oldPath);
13439
+ if (newPath) paths.push(newPath);
13440
+ }
13441
+ return paths;
13442
+ }
13443
+ function appendTarget(targets, value, kind, base) {
13444
+ if (typeof value !== "string" || value.length === 0) return;
13445
+ targets.push({
13446
+ path: resolveTargetPath(value, base),
13447
+ kind
13448
+ });
13449
+ }
13450
+ function appendTargetList(targets, value, directoryCapable, base) {
13451
+ const appendPath = (path) => {
13452
+ targets.push({
13453
+ path: resolveTargetPath(path, base),
13454
+ kind: isUnresolvedPathScope(path) || isRootPathScope(path) || directoryCapable && isDirectoryAmbiguousPath(path) ? "scope" : "file"
13455
+ });
13456
+ };
13457
+ if (typeof value === "string") {
13458
+ const paths = value.split(",").map((item) => item.trim()).filter((path) => path.length > 0);
13459
+ for (const path of paths) appendPath(path);
13460
+ return;
13461
+ }
13462
+ if (!Array.isArray(value)) return;
13463
+ for (const item of value) {
13464
+ if (typeof item !== "string") continue;
13465
+ const path = item.trim();
13466
+ if (path.length > 0) appendPath(path);
13467
+ }
13468
+ }
13469
+ function isReadOnlyInvocation(toolName, toolInput) {
13470
+ if (toolName === "git") {
13471
+ const command = toolInput["command"];
13472
+ if (command === "status" || command === "log" || command === "diff" || command === "fetch" || command === "commit" || command === "branch" || command === "push") {
13473
+ return true;
13474
+ }
13475
+ return command === "worktree" && toolInput["worktreeAction"] === "list";
13476
+ }
13477
+ if (toolName === "design") {
13478
+ const action = toolInput["action"] ?? "list";
13479
+ return action === "list" || action === "foundations" || action === "verify";
13480
+ }
13481
+ if (toolName === "format") return toolInput["check"] === true;
13482
+ if (toolName === "patch" || toolName === "scaffold") return toolInput["dry_run"] === true;
13483
+ if (toolName === "replace") return toolInput["dry_run"] !== false;
13484
+ return false;
13485
+ }
13486
+ function pathsFromToolInput(toolInput, toolName, invocationCwd) {
13487
+ const targets = [];
13488
+ const effectiveCwd = effectiveToolCwd(toolInput["cwd"], invocationCwd);
13489
+ const directoryDelete = isDirectoryDeleteToolName(toolName);
13490
+ for (const field of PATH_FIELDS) {
13491
+ const value = toolInput[field];
13492
+ appendTarget(
13493
+ targets,
13494
+ value,
13495
+ directoryDelete ? "deletion-scope" : typeof value === "string" && isUnresolvedPathScope(value) ? "scope" : "file",
13496
+ effectiveCwd
13497
+ );
13498
+ }
13499
+ const directoryCapableLists = toolName === "format" || toolName === "replace";
13500
+ for (const field of PATH_LIST_FIELDS) {
13501
+ appendTargetList(
13502
+ targets,
13503
+ toolInput[field],
13504
+ directoryCapableLists && field === "files",
13505
+ effectiveCwd
13506
+ );
13507
+ }
13508
+ if (toolName === "install") {
13509
+ appendTarget(targets, effectiveCwd ?? invocationCwd ?? ".", "scope");
13510
+ }
13511
+ if (toolName === "git") {
13512
+ const command = toolInput["command"];
13513
+ if (command === "worktree" && (toolInput["worktreeAction"] === "add" || toolInput["worktreeAction"] === "remove")) {
13514
+ appendTarget(targets, toolInput["worktreePath"] ?? toolInput["worktree_path"], "file");
13515
+ }
13516
+ }
13517
+ if (toolName === "scaffold") {
13518
+ const cwdValue = toolInput["cwd"];
13519
+ const cwd = typeof cwdValue === "string" && cwdValue.length > 0 ? cwdValue : invocationCwd ?? ".";
13520
+ const base = normalizePath2(cwd).replace(/\/$/, "");
13521
+ const name = typeof toolInput["name"] === "string" ? toolInput["name"] : "";
13522
+ const template = toolInput["template"];
13523
+ const filesByTemplate = {
13524
+ "npm-package": ["package.json", "tsconfig.json", "src/index.ts", "src/index.test.ts"],
13525
+ "cli-tool": ["package.json", "src/index.ts"],
13526
+ "react-component": name ? [`${name}.tsx`, `${name}.test.tsx`] : []
13527
+ };
13528
+ for (const file of typeof template === "string" ? filesByTemplate[template] ?? [] : []) {
13529
+ targets.push({ path: `${base}/${file}`, kind: "file" });
13530
+ }
13531
+ targets.push({ path: base, kind: "scope" });
13532
+ }
13533
+ if (isMoveToolName(toolName)) {
13534
+ for (const field of SOURCE_PATH_FIELDS) {
13535
+ appendTarget(targets, toolInput[field], "deletion-scope", effectiveCwd);
13536
+ }
13537
+ }
13538
+ const patch = toolInput["patch"];
13539
+ if (typeof patch === "string") {
13540
+ const patchTargets = pathsFromPatch(patch);
13541
+ const directory = toolInput["directory"];
13542
+ const baseValue = typeof directory === "string" && directory.length > 0 ? directory : invocationCwd;
13543
+ if (baseValue) {
13544
+ const base = normalizePath2(baseValue).replace(/\/$/, "");
13545
+ targets.push(
13546
+ ...patchTargets.map((target) => ({ path: `${base}/${target}`, kind: "file" }))
13547
+ );
13548
+ } else {
13549
+ targets.push(...patchTargets.map((path) => ({ path, kind: "file" })));
13550
+ }
13551
+ }
13552
+ if (targets.length === 0) {
13553
+ const cwd = effectiveCwd ?? ".";
13554
+ targets.push({ path: normalizePath2(cwd).replace(/\/$/, "") || ".", kind: "scope" });
13555
+ }
13556
+ const seen = /* @__PURE__ */ new Set();
13557
+ return targets.map((target) => ({
13558
+ ...target,
13559
+ path: relativeToInvocationCwd(target.path, invocationCwd)
13560
+ })).filter((target) => {
13561
+ const key = `${target.kind}:${target.path}`;
13562
+ if (seen.has(key)) return false;
13563
+ seen.add(key);
13564
+ return true;
13565
+ });
13566
+ }
13567
+ function operationLabel(toolName) {
13568
+ if (toolName === "write") return "write";
13569
+ if (toolName === "edit") return "edit";
13570
+ return `write via "${toolName}"`;
13571
+ }
13572
+ var VALUE_TAKING_GIT_OPTIONS = /* @__PURE__ */ new Set([
13573
+ "-C",
13574
+ "-c",
13575
+ "--git-dir",
13576
+ "--work-tree",
13577
+ "--namespace",
13578
+ "--super-prefix",
13579
+ "--exec-path",
13580
+ "--config-env",
13581
+ "--attr-source"
13582
+ ]);
13583
+ var MAX_LAUNCHER_TOKENS = 256;
13584
+ var MAX_LAUNCHER_LENGTH = 64 * 1024;
13585
+ function boundedShellTokens(raw) {
13586
+ const tokens = [];
13587
+ let token = "";
13588
+ let tokenStart = -1;
13589
+ let quote = null;
13590
+ const limit = Math.min(raw.length, MAX_LAUNCHER_LENGTH);
13591
+ for (let index = 0; index < limit && tokens.length < MAX_LAUNCHER_TOKENS; index += 1) {
13592
+ const char = raw[index] ?? "";
13593
+ if (tokenStart < 0 && !/\s/.test(char)) tokenStart = index;
13594
+ if (char === "\\" && quote !== "'" && index + 1 < limit) {
13595
+ const escaped = raw[index + 1] ?? "";
13596
+ const shellEscaped = quote === '"' ? /[$\x60"\\\r\n]/.test(escaped) : /[\s'"\\;&|()`]/.test(escaped);
13597
+ token += shellEscaped ? escaped : `\\${escaped}`;
13598
+ index += 1;
13599
+ continue;
13600
+ }
13601
+ if (char === "'" || char === '"') {
13602
+ if (quote === char) quote = null;
13603
+ else if (quote === null) quote = char;
13604
+ else token += char;
13605
+ continue;
13606
+ }
13607
+ if (quote === null && /\s/.test(char)) {
13608
+ if (tokenStart >= 0) tokens.push({ value: token, start: tokenStart, end: index });
13609
+ token = "";
13610
+ tokenStart = -1;
13611
+ continue;
13612
+ }
13613
+ token += char;
13614
+ }
13615
+ if (tokenStart >= 0 && tokens.length < MAX_LAUNCHER_TOKENS) {
13616
+ tokens.push({ value: token, start: tokenStart, end: limit });
13617
+ }
13618
+ return tokens;
13619
+ }
13620
+ function shellTokens(raw) {
13621
+ return boundedShellTokens(raw).map((token) => token.value);
13622
+ }
13623
+ function gitInvocationArguments(command) {
13624
+ const argumentsList = [];
13625
+ const quotedIndexes = new Uint8Array(command.length);
13626
+ let activeQuote = null;
13627
+ for (let index = 0; index < command.length; index += 1) {
13628
+ const char = command[index];
13629
+ if (isQuoteBoundary(command, index, activeQuote)) {
13630
+ quotedIndexes[index] = 1;
13631
+ activeQuote = activeQuote === char ? null : char === "'" ? "'" : '"';
13632
+ } else if (activeQuote !== null) {
13633
+ quotedIndexes[index] = 1;
13634
+ }
13635
+ }
13636
+ const gitStart = /(?:^|[;&|\r\n]\s*|\(\s*|`\s*)git\b/gi;
13637
+ let match = gitStart.exec(command);
13638
+ while (match !== null) {
13639
+ const gitOffset = match[0].toLowerCase().lastIndexOf("git");
13640
+ if (gitOffset < 0 || quotedIndexes[match.index + gitOffset] === 1) {
13641
+ match = gitStart.exec(command);
13642
+ continue;
13643
+ }
13644
+ const start = gitStart.lastIndex;
13645
+ let quote = null;
13646
+ let end = start;
13647
+ for (; end < command.length; end += 1) {
13648
+ const char = command[end] ?? "";
13649
+ if (char === "\\" && quote !== "'") {
13650
+ end += 1;
13651
+ continue;
13652
+ }
13653
+ if (char === "'" || char === '"') {
13654
+ if (quote === char) quote = null;
13655
+ else if (quote === null) quote = char;
13656
+ continue;
13657
+ }
13658
+ if (quote === null && /[;&|)`\r\n]/.test(char)) break;
13659
+ }
13660
+ argumentsList.push(command.slice(start, end));
13661
+ gitStart.lastIndex = Math.max(end, start);
13662
+ match = gitStart.exec(command);
13663
+ }
13664
+ return argumentsList;
13665
+ }
13666
+ function gitSubcommandIndex(tokens) {
13667
+ for (let index = 0; index < tokens.length; index += 1) {
13668
+ const token = tokens[index] ?? "";
13669
+ if (token === "--") return index + 1 < tokens.length ? index + 1 : -1;
13670
+ if (!token.startsWith("-")) return index;
13671
+ const optionName = token.startsWith("-C") && token !== "-C" ? "-C" : token.split("=", 1)[0] ?? token;
13672
+ const hasAttachedValue = token.includes("=") || token.startsWith("-C") && token !== "-C";
13673
+ if (VALUE_TAKING_GIT_OPTIONS.has(optionName) && !hasAttachedValue) index += 1;
13674
+ }
13675
+ return -1;
13676
+ }
13677
+ function commandDeletesImplicitScope(command) {
13678
+ const stripped = stripTransparentLaunchers(maskNonExecutingHeredocBodies(command));
13679
+ for (const rawArguments of gitInvocationArguments(stripped)) {
13680
+ const tokens = shellTokens(rawArguments);
13681
+ const commandIndex = gitSubcommandIndex(tokens);
13682
+ const subcommand = commandIndex >= 0 ? tokens[commandIndex]?.toLowerCase() : void 0;
13683
+ const operands = tokens.slice(commandIndex + 1);
13684
+ if (/^(?:clean|restore|reset|checkout|switch)$/.test(subcommand ?? "")) return true;
13685
+ if (subcommand === "stash") {
13686
+ const action = operands.find((token) => !token.startsWith("-"))?.toLowerCase();
13687
+ if (action === void 0 || /^(?:push|save|pop|apply)$/.test(action)) return true;
13688
+ }
13689
+ }
13690
+ return false;
13691
+ }
13692
+ function normalizeEnvSplitPayload(payload) {
13693
+ const removeUnbalanced = (value, quote) => {
13694
+ let boundaries = 0;
13695
+ for (let index = 0; index < value.length; index += 1) {
13696
+ if (value[index] === quote && !quoteIsEscaped(value, index)) boundaries += 1;
13697
+ }
13698
+ return boundaries % 2 === 0 ? value : value.replaceAll(quote, "");
13699
+ };
13700
+ return removeUnbalanced(removeUnbalanced(payload, "'"), '"');
13701
+ }
13702
+ function unwrapEnvSplitStringAtBoundary(command) {
13703
+ const pattern = /(^|[;&|\r\n]\s*|\(\s*|`\s*)(?:[^\s;&|(){}]+[\\/])?env\b/gi;
13704
+ const match = pattern.exec(command);
13705
+ if (!match) return command;
13706
+ const boundary = match[1] ?? "";
13707
+ const afterStart = match.index + match[0].length;
13708
+ const after = command.slice(afterStart);
13709
+ const tokens = boundedShellTokens(after);
13710
+ for (let index = 0; index < tokens.length; index += 1) {
13711
+ const token = tokens[index];
13712
+ if (!token) continue;
13713
+ const separator = firstUnquotedShellSeparator(after, token.start, token.end);
13714
+ const tokenEnd = separator ?? token.end;
13715
+ const tokenValue = boundedShellTokens(after.slice(token.start, tokenEnd))[0]?.value ?? "";
13716
+ const attachedShort = /^-[i0v]*S(.+)$/.exec(tokenValue)?.[1];
13717
+ const attachedLong = tokenValue.startsWith("--split-string=") ? tokenValue.slice("--split-string=".length) : void 0;
13718
+ const attachedPayload = attachedShort ?? attachedLong;
13719
+ if (attachedPayload !== void 0) {
13720
+ return `${command.slice(0, match.index)}${boundary}${normalizeEnvSplitPayload(attachedPayload)}${after.slice(tokenEnd)}`;
13721
+ }
13722
+ if (!/^-[i0v]*S$/.test(tokenValue) && tokenValue !== "--split-string") {
13723
+ if (separator !== void 0) return command;
13724
+ continue;
13725
+ }
13726
+ const payload = tokens[index + 1];
13727
+ if (!payload) return command;
13728
+ const payloadSeparator = firstUnquotedShellSeparator(after, payload.start, payload.end);
13729
+ const payloadEnd = payloadSeparator ?? payload.end;
13730
+ const payloadValue = boundedShellTokens(after.slice(payload.start, payloadEnd))[0]?.value ?? "";
13731
+ return `${command.slice(0, match.index)}${boundary}${normalizeEnvSplitPayload(payloadValue)}${after.slice(payloadEnd)}`;
13732
+ }
13733
+ return command;
13734
+ }
13735
+ function stripTransparentLaunchers(command) {
13736
+ let stripped = command;
13737
+ let previous;
13738
+ do {
13739
+ previous = stripped;
13740
+ let beforeUnwrap;
13741
+ do {
13742
+ beforeUnwrap = stripped;
13743
+ stripped = unwrapEnvSplitStringAtBoundary(stripped);
13744
+ } while (stripped !== beforeUnwrap);
13745
+ stripped = stripLauncherAtBoundary(stripped, "env", ENV_VALUE_TAKING);
13746
+ stripped = stripLauncherAtBoundary(stripped, "sudo", SUDO_VALUE_TAKING);
13747
+ } while (stripped !== previous);
13748
+ return stripped;
13749
+ }
13750
+ var SUDO_VALUE_TAKING = /* @__PURE__ */ new Set([
13751
+ "-u",
13752
+ "-g",
13753
+ "-h",
13754
+ "-p",
13755
+ "-C",
13756
+ "-R",
13757
+ "-D",
13758
+ "-r",
13759
+ "-t",
13760
+ "--user",
13761
+ "--group",
13762
+ "--host",
13763
+ "--prompt",
13764
+ "--close-from",
13765
+ "--chroot",
13766
+ "--chdir",
13767
+ "--role",
13768
+ "--type",
13769
+ "--command-timeout"
13770
+ ]);
13771
+ var ENV_VALUE_TAKING = /* @__PURE__ */ new Set([
13772
+ "-u",
13773
+ "-C",
13774
+ "-P",
13775
+ "-a",
13776
+ "--argv0",
13777
+ "--unset",
13778
+ "--chdir",
13779
+ "--split-string"
13780
+ ]);
13781
+ var ENV_FLAG_OPTIONS = /* @__PURE__ */ new Set([
13782
+ "-i",
13783
+ "-0",
13784
+ "-v",
13785
+ "--ignore-environment",
13786
+ "--null",
13787
+ "--debug",
13788
+ "--help",
13789
+ "--version"
13790
+ ]);
13791
+ function firstUnquotedShellSeparator(raw, start, end) {
13792
+ let quote = null;
13793
+ for (let index = start; index < end; index += 1) {
13794
+ const char = raw[index];
13795
+ if (isQuoteBoundary(raw, index, quote)) {
13796
+ quote = quote === char ? null : char === "'" ? "'" : '"';
13797
+ continue;
13798
+ }
13799
+ if (quote === null && !quoteIsEscaped(raw, index) && /[;&|]/.test(char ?? "")) return index;
13800
+ }
13801
+ return void 0;
13802
+ }
13803
+ function launcherPrefixLength(after, valueTaking, flagOptions) {
13804
+ const tokens = boundedShellTokens(after);
13805
+ let consumed = 0;
13806
+ for (let i = 0; i < tokens.length; i += 1) {
13807
+ const tok = tokens[i] ?? { value: "", start: 0, end: 0 };
13808
+ const token = tok.value;
13809
+ const separator = firstUnquotedShellSeparator(after, tok.start, tok.end);
13810
+ if (separator !== void 0) return consumed || separator;
13811
+ if (token === "--") return tok.end;
13812
+ if (/^[A-Za-z_][A-Za-z0-9_]*=/.test(token)) {
13813
+ consumed = tok.end;
13814
+ continue;
13815
+ }
13816
+ if (token === "-" || !token.startsWith("-")) break;
13817
+ const optionName = token.split("=", 1)[0] ?? token;
13818
+ const combinedValueOption = [...valueTaking].find(
13819
+ (option) => option.length === 2 && token.startsWith(option) && token !== option
13820
+ );
13821
+ const combinedShortValueOption = token.startsWith("-") && !token.startsWith("--") ? [...valueTaking].find(
13822
+ (option) => option.length === 2 && token.includes(option[1] ?? "", 2)
13823
+ ) : void 0;
13824
+ const hasAttachedValue = token.includes("=") || combinedValueOption !== void 0;
13825
+ if (!hasAttachedValue && (valueTaking.has(optionName) || combinedShortValueOption !== void 0)) {
13826
+ if (i + 1 >= tokens.length) return flagOptions ? void 0 : tok.end;
13827
+ const valueToken = tokens[i + 1] ?? tok;
13828
+ const valueSeparator = firstUnquotedShellSeparator(after, valueToken.start, valueToken.end);
13829
+ if (valueSeparator !== void 0) return consumed || valueSeparator;
13830
+ consumed = valueToken.end;
13831
+ i += 1;
13832
+ } else {
13833
+ const knownFlag = flagOptions?.has(optionName) || flagOptions !== void 0 && /^-[i0v]+$/.test(token);
13834
+ const knownAttachedValue = hasAttachedValue && (valueTaking.has(optionName) || combinedValueOption !== void 0);
13835
+ if (flagOptions && !knownFlag && !knownAttachedValue) return void 0;
13836
+ consumed = tok.end;
13837
+ }
13838
+ }
13839
+ return consumed;
13840
+ }
13841
+ function stripLauncherAtBoundary(command, launcher, valueTaking) {
13842
+ const pattern = new RegExp(
13843
+ `(^|[;&|\\r\\n]\\s*|\\(\\s*|\`\\s*)(?:[^\\s;&|(){}]+[\\\\/])?${launcher}\\b`,
13844
+ "i"
13845
+ );
13846
+ const match = pattern.exec(command);
13847
+ if (!match) return command;
13848
+ const boundary = match[1] ?? "";
13849
+ const afterStart = match.index + match[0].length;
13850
+ const after = command.slice(afterStart);
13851
+ const consumed = launcherPrefixLength(
13852
+ after,
13853
+ valueTaking,
13854
+ launcher === "env" ? ENV_FLAG_OPTIONS : void 0
13855
+ );
13856
+ if (consumed === void 0) {
13857
+ return `${command.slice(0, match.index)}${boundary}rm -rf **`;
13858
+ }
13859
+ return `${command.slice(0, match.index)}${boundary}${after.slice(consumed).replace(/^\s+/, "")}`;
13860
+ }
13861
+ function commandRecursivelyDeletes(command) {
13862
+ const stripped = stripTransparentLaunchers(maskNonExecutingHeredocBodies(command));
13863
+ if (/(?:^|[;&|\r\n]\s*|\$\(\s*|\(\s*|`\s*)find\b[^;&|)`]*(?:-delete\b|-exec(?:dir)?\s+(?:[^\s;&|]+[\\/])?(?:rm|rmdir)\b)/i.test(
13864
+ stripped
13865
+ )) {
13866
+ return true;
13867
+ }
13868
+ const destructive = /(?:^|[;&|\r\n]\s*|\{\s*|\(\s*|\bxargs(?:\s+-[^\s]+)*\s+)(rm|rmdir|del)\s+((?:"[^"]*"|'[^']*'|\\.|\{[^}]*\}|\([^()]*\)|[^;&|\r\n}])+)/gi;
13869
+ let match = destructive.exec(stripped);
13870
+ while (match !== null) {
13871
+ const tool = match[1]?.toLowerCase();
13872
+ if (tool === "rmdir") return true;
13873
+ const tokens = (match[2]?.match(/"[^"]*"|'[^']*'|[^\s]+/g) ?? []).map(
13874
+ (arg) => arg.replace(/^["']|["']$/g, "")
13875
+ );
13876
+ let recursive = false;
13877
+ for (const token of tokens) {
13878
+ if (token === "--") break;
13879
+ if (tool === "rm") {
13880
+ if (token === "--recursive" || /^-[^-]*[rR]/.test(token)) recursive = true;
13881
+ } else if (/^\/[a-z]*s[a-z]*$/i.test(token)) {
13882
+ recursive = true;
13883
+ }
13884
+ }
13885
+ if (recursive) return true;
13886
+ match = destructive.exec(stripped);
13887
+ }
13888
+ return false;
13889
+ }
13890
+ function quoteIsEscaped(command, index) {
13891
+ let backslashes = 0;
13892
+ for (let cursor = index - 1; cursor >= 0 && command[cursor] === "\\"; cursor -= 1) {
13893
+ backslashes += 1;
13894
+ }
13895
+ return backslashes % 2 === 1;
13896
+ }
13897
+ function isQuoteBoundary(command, index, activeQuote) {
13898
+ const char = command[index];
13899
+ if (char !== "'" && char !== '"') return false;
13900
+ if (activeQuote === "'") return char === "'";
13901
+ if (activeQuote !== null && char !== activeQuote) return false;
13902
+ return !quoteIsEscaped(command, index);
13903
+ }
13904
+ function executableCommandSubstitutions(command) {
13905
+ const bodies = [];
13906
+ let outerQuote = null;
13907
+ for (let index = 0; index < command.length; index += 1) {
13908
+ const char = command[index];
13909
+ if (isQuoteBoundary(command, index, outerQuote)) {
13910
+ outerQuote = outerQuote === char ? null : char === "'" ? "'" : '"';
13911
+ continue;
13912
+ }
13913
+ if (outerQuote === "'" || quoteIsEscaped(command, index)) continue;
13914
+ if (char === "`") {
13915
+ let end2 = index + 1;
13916
+ while (end2 < command.length && (command[end2] !== "`" || quoteIsEscaped(command, end2))) {
13917
+ end2 += 1;
13918
+ }
13919
+ if (end2 < command.length) {
13920
+ bodies.push(command.slice(index + 1, end2));
13921
+ index = end2;
13922
+ }
13923
+ continue;
13924
+ }
13925
+ const commandSubstitution = char === "$" && command[index + 1] === "(";
13926
+ const processSubstitution = (char === ">" || char === "<") && command[index + 1] === "(";
13927
+ if (!commandSubstitution && !processSubstitution || // Arithmetic expansion evaluates an expression; it does not execute its
13928
+ // contents as a shell command substitution.
13929
+ commandSubstitution && command[index + 2] === "(")
13930
+ continue;
13931
+ let depth = 1;
13932
+ let innerQuote = null;
13933
+ let end = index + 2;
13934
+ for (; end < command.length; end += 1) {
13935
+ const innerChar = command[end];
13936
+ if (isQuoteBoundary(command, end, innerQuote)) {
13937
+ innerQuote = innerQuote === innerChar ? null : innerChar === "'" ? "'" : '"';
13938
+ continue;
13939
+ }
13940
+ if (innerQuote !== null) continue;
13941
+ if ((innerChar === "(" || innerChar === ")") && quoteIsEscaped(command, end)) continue;
13942
+ if (innerChar === "(") depth += 1;
13943
+ else if (innerChar === ")") {
13944
+ depth -= 1;
13945
+ if (depth === 0) break;
13946
+ }
13947
+ }
13948
+ if (depth === 0) {
13949
+ bodies.push(command.slice(index + 2, end));
13950
+ index = end;
13951
+ } else {
13952
+ break;
13953
+ }
13954
+ }
13955
+ return bodies;
13956
+ }
13957
+ var MAX_DESTRUCTIVE_TARGET_DEPTH = 64;
13958
+ function heredocDelimiterOnLine(line) {
13959
+ let quote = null;
13960
+ for (let index = 0; index < line.length - 1; index += 1) {
13961
+ const char = line[index];
13962
+ if (isQuoteBoundary(line, index, quote)) {
13963
+ quote = quote === char ? null : char === "'" ? "'" : '"';
13964
+ continue;
13965
+ }
13966
+ if (quote !== null || quoteIsEscaped(line, index) || char !== "<" || line[index - 1] === "<" || line[index + 1] !== "<" || line[index + 2] === "<")
13967
+ continue;
13968
+ let cursor = index + 2;
13969
+ const stripTabs = line[cursor] === "-";
13970
+ if (stripTabs) cursor += 1;
13971
+ while (line[cursor] === " " || line[cursor] === " ") cursor += 1;
13972
+ let delimiter2 = "";
13973
+ let delimiterQuote = null;
13974
+ let quoted = false;
13975
+ for (; cursor < line.length; cursor += 1) {
13976
+ const delimiterChar = line[cursor] ?? "";
13977
+ if (delimiterQuote !== null) {
13978
+ if (delimiterChar === delimiterQuote && !quoteIsEscaped(line, cursor)) {
13979
+ delimiterQuote = null;
13980
+ quoted = true;
13981
+ } else if (delimiterChar === "\\" && delimiterQuote === '"' && cursor + 1 < line.length) {
13982
+ quoted = true;
13983
+ cursor += 1;
13984
+ delimiter2 += line[cursor] ?? "";
13985
+ } else {
13986
+ delimiter2 += delimiterChar;
13987
+ }
13988
+ continue;
13989
+ }
13990
+ if (delimiterChar === "'" || delimiterChar === '"') {
13991
+ delimiterQuote = delimiterChar;
13992
+ quoted = true;
13993
+ continue;
13994
+ }
13995
+ if (delimiterChar === "\\" && cursor + 1 < line.length) {
13996
+ quoted = true;
13997
+ cursor += 1;
13998
+ delimiter2 += line[cursor] ?? "";
13999
+ continue;
14000
+ }
14001
+ if (/\s|[;&|<>]/.test(delimiterChar)) break;
14002
+ delimiter2 += delimiterChar;
14003
+ }
14004
+ return delimiter2.length > 0 ? { delimiter: delimiter2, start: index, end: cursor, quoted, stripTabs } : null;
14005
+ }
14006
+ return null;
14007
+ }
14008
+ function commandSegmentBeforeHeredoc(prefix) {
14009
+ let segmentStart = 0;
14010
+ let quote = null;
14011
+ for (let index = 0; index < prefix.length; index += 1) {
14012
+ const char = prefix[index];
14013
+ if (isQuoteBoundary(prefix, index, quote)) {
14014
+ quote = quote === char ? null : char === "'" ? "'" : '"';
14015
+ continue;
14016
+ }
14017
+ if (quote === null && !quoteIsEscaped(prefix, index) && /[;&|\r\n]/.test(char ?? "")) {
14018
+ segmentStart = index + 1;
14019
+ }
14020
+ }
14021
+ return prefix.slice(segmentStart).trim();
14022
+ }
14023
+ function maskNonExecutingHeredocBodies(command) {
14024
+ const lines = command.split(/(?<=\n)/);
14025
+ let heredoc = null;
14026
+ const maskBody = (start, end, quoted) => {
14027
+ const body = lines.slice(start, end).join("");
14028
+ const substitutions = quoted ? [] : executableCommandSubstitutions(body);
14029
+ for (let index = start; index < end; index += 1) {
14030
+ const line = lines[index] ?? "";
14031
+ lines[index] = line.endsWith("\r\n") ? "\r\n" : line.endsWith("\n") ? "\n" : "";
14032
+ }
14033
+ if (substitutions.length > 0 && start < end)
14034
+ lines[start] = `${substitutions.join(";")}${lines[start] ?? ""}`;
14035
+ };
14036
+ for (let index = 0; index < lines.length; index += 1) {
14037
+ const line = lines[index] ?? "";
14038
+ if (heredoc !== null) {
14039
+ const content = line.replace(/\r?\n$/, "");
14040
+ const terminator = heredoc.stripTabs ? content.replace(/^\t+/, "") : content;
14041
+ if (terminator === heredoc.delimiter) {
14042
+ maskBody(heredoc.bodyStart, index, heredoc.quoted);
14043
+ heredoc = null;
14044
+ }
14045
+ continue;
14046
+ }
14047
+ const marker = heredocDelimiterOnLine(line);
14048
+ if (!marker) continue;
14049
+ const prefix = line.slice(0, marker.start).trim();
14050
+ const suffix = line.slice(marker.end).trim();
14051
+ const owner = stripTransparentLaunchers(commandSegmentBeforeHeredoc(prefix));
14052
+ const commandBeforeMarker = `${lines.slice(0, index).join("")}${prefix}`;
14053
+ const whileReadLoopOwnsHeredoc = /^done$/i.test(owner) && /(?:^|[;&|\r\n])\s*while\s+read(?:\s|$)[\s\S]*\bdone\s*$/i.test(commandBeforeMarker);
14054
+ const receivesDataWithoutExecuting = /^(?:[^\s;&|]+[\\/])?(?:cat|tee)(?:\s|$)/i.test(owner) || /^(?:while\s+)?read(?:\s|$)/i.test(owner) || whileReadLoopOwnsHeredoc;
14055
+ const redirectedFileMatch = /^(?:>>|>\||>)\s*("[^"]+"|'[^']+'|[^\s;&|<>]+)/.exec(suffix);
14056
+ const redirectedFile = redirectedFileMatch?.[1]?.replace(/^['"]|['"]$/g, "");
14057
+ const remainingCommand = lines.slice(index + 1).join("");
14058
+ const executionSearch = `${suffix}
14059
+ ${remainingCommand}`;
14060
+ const normalizedRedirectedFile = redirectedFile ? normalizePath2(redirectedFile) : void 0;
14061
+ const executesRedirectedFile = normalizedRedirectedFile !== void 0 && boundedShellTokens(executionSearch).some((token) => {
14062
+ if (normalizePath2(token.value) !== normalizedRedirectedFile) return false;
14063
+ const segmentStart = Math.max(
14064
+ executionSearch.lastIndexOf(";", token.start - 1),
14065
+ executionSearch.lastIndexOf("&", token.start - 1),
14066
+ executionSearch.lastIndexOf("|", token.start - 1),
14067
+ executionSearch.lastIndexOf("\n", token.start - 1),
14068
+ executionSearch.lastIndexOf("\r", token.start - 1)
14069
+ );
14070
+ const segmentTokens = shellTokens(executionSearch.slice(segmentStart + 1, token.start));
14071
+ const previous = segmentTokens.at(-1)?.toLowerCase();
14072
+ return previous === void 0 || /^(?:(?:ba|z|k)?sh|source|\.)$/.test(previous);
14073
+ });
14074
+ const processSubstitution = /^(?:>>|>\||>)\s*>\s*\(\s*([^)]*)/.exec(suffix);
14075
+ const processCommand = boundedShellTokens(
14076
+ stripTransparentLaunchers(processSubstitution?.[1]?.trim() ?? "")
14077
+ )[0]?.value.replace(/^.*[\\/]/, "").toLowerCase();
14078
+ const executesBody = /^(?:\||;|&|\(|\{)/.test(suffix) || /^(?:(?:ba|z|k)?sh|source|\.)$/.test(processCommand ?? "") || executesRedirectedFile;
14079
+ if (!receivesDataWithoutExecuting || executesBody) continue;
14080
+ heredoc = { ...marker, bodyStart: index + 1 };
14081
+ }
14082
+ if (heredoc !== null) maskBody(heredoc.bodyStart, lines.length, heredoc.quoted);
14083
+ return lines.join("");
14084
+ }
13194
14085
  function destructiveTargets(command) {
14086
+ return destructiveTargetsAtDepth(command, 0);
14087
+ }
14088
+ function destructiveTargetsAtDepth(command, depth) {
14089
+ if (depth >= MAX_DESTRUCTIVE_TARGET_DEPTH) return ["**"];
14090
+ const normalizedCommand = stripTransparentLaunchers(maskNonExecutingHeredocBodies(command));
13195
14091
  const targets = [];
13196
- const destructive = /(?:^|[;&|]\s*)(?:sudo\s+)?(rm|rmdir|del|unlink|truncate|shred|mv)\s+([^;&|]+)/gi;
13197
- let m = destructive.exec(command);
14092
+ const quotedIndexes = new Uint8Array(normalizedCommand.length);
14093
+ let activeQuote = null;
14094
+ for (let index = 0; index < normalizedCommand.length; index += 1) {
14095
+ const char = normalizedCommand[index];
14096
+ if (isQuoteBoundary(normalizedCommand, index, activeQuote)) {
14097
+ quotedIndexes[index] = 1;
14098
+ activeQuote = activeQuote === char ? null : char === "'" ? "'" : '"';
14099
+ } else if (activeQuote !== null) {
14100
+ quotedIndexes[index] = 1;
14101
+ }
14102
+ }
14103
+ const tokenIsQuoted = (match, token) => {
14104
+ const offset = match[0].toLowerCase().indexOf(token.toLowerCase());
14105
+ return offset >= 0 && quotedIndexes[match.index + offset] === 1;
14106
+ };
14107
+ const shellOperandTokens = (raw) => {
14108
+ const tokens = shellTokens(raw);
14109
+ const redirectIndex = tokens.findIndex((token) => /^(?:\d*(?:<>|>>?|<)|&>>?)/.test(token));
14110
+ return redirectIndex === -1 ? tokens : tokens.slice(0, redirectIndex);
14111
+ };
14112
+ const shellArgs = (raw) => {
14113
+ const lastNonWhitespace = raw.search(/\s*$/) - 1;
14114
+ let quote2 = null;
14115
+ for (let index = 0; index < lastNonWhitespace; index += 1) {
14116
+ if (isQuoteBoundary(raw, index, quote2)) {
14117
+ const char = raw[index];
14118
+ quote2 = quote2 === char ? null : char === "'" ? "'" : '"';
14119
+ }
14120
+ }
14121
+ const args = raw[lastNonWhitespace] === ")" && quote2 === null && !quoteIsEscaped(raw, lastNonWhitespace) ? raw.slice(0, lastNonWhitespace) : raw;
14122
+ return shellOperandTokens(args).filter((arg) => arg.length > 0 && !arg.startsWith("-"));
14123
+ };
14124
+ for (const body of executableCommandSubstitutions(normalizedCommand)) {
14125
+ let executableBody = body.trim();
14126
+ while (executableBody.startsWith("(") && executableBody.endsWith(")")) {
14127
+ executableBody = executableBody.slice(1, -1).trim();
14128
+ }
14129
+ targets.push(...destructiveTargetsAtDepth(executableBody, depth + 1));
14130
+ }
14131
+ const destructive = /(?:^|[;&|\r\n]\s*|\{\s*|(?<![$(])\(\s*|\bxargs(?:\s+-[^\s]+)*\s+)(?:sudo\s+)?(rm|rmdir|del|unlink|truncate|shred|mv)\s+((?:"[^"]*"|'[^']*'|\\.|\{[^}]*\}|\([^()]*\)|[^;&|\r\n}])+)/gi;
14132
+ let m = destructive.exec(normalizedCommand);
13198
14133
  while (m !== null) {
13199
- const args = (m[2] ?? "").split(/\s+/).filter((a) => a.length > 0 && !a.startsWith("-"));
13200
- targets.push(...args);
13201
- m = destructive.exec(command);
13202
- }
13203
- const redirect = />{1,2}\s*([^\s;&|>]+)/g;
13204
- let r = redirect.exec(command);
13205
- while (r !== null) {
13206
- const target = r[1];
13207
- if (target && target !== "/dev/null" && target !== "NUL" && target !== "nul") {
14134
+ if (!tokenIsQuoted(m, m[1] ?? "")) targets.push(...shellArgs(m[2] ?? ""));
14135
+ m = destructive.exec(normalizedCommand);
14136
+ }
14137
+ const copy = /(?:^|[;&|\r\n]\s*)(?:sudo\s+)?(cp|install)\s+([^;&|\r\n]+)/gi;
14138
+ let c = copy.exec(normalizedCommand);
14139
+ while (c !== null) {
14140
+ if (tokenIsQuoted(c, c[1] ?? "")) {
14141
+ c = copy.exec(normalizedCommand);
14142
+ continue;
14143
+ }
14144
+ const tokens = shellTokens(c[2] ?? "");
14145
+ let destination;
14146
+ for (let index = 0; index < tokens.length; index += 1) {
14147
+ const token = tokens[index];
14148
+ if (token === "-t" || token === "--target-directory") {
14149
+ destination = tokens[index + 1];
14150
+ break;
14151
+ }
14152
+ if (token?.startsWith("--target-directory=")) {
14153
+ destination = token.slice("--target-directory=".length);
14154
+ break;
14155
+ }
14156
+ }
14157
+ destination ??= tokens.filter((arg) => arg.length > 0 && !arg.startsWith("-")).at(-1);
14158
+ if (destination) targets.push(destination);
14159
+ c = copy.exec(normalizedCommand);
14160
+ }
14161
+ const tee = /(?:^|[;&|\r\n]\s*)(?:sudo\s+)?(tee)\s+([^;&|\r\n]+)/gi;
14162
+ let t = tee.exec(normalizedCommand);
14163
+ while (t !== null) {
14164
+ if (!tokenIsQuoted(t, t[1] ?? "")) targets.push(...shellArgs(t[2] ?? ""));
14165
+ t = tee.exec(normalizedCommand);
14166
+ }
14167
+ const dd = /(?:^|[;&|\r\n]\s*)(?:sudo\s+)?(dd)\s+([^;&|\r\n]+)/gi;
14168
+ let d = dd.exec(normalizedCommand);
14169
+ while (d !== null) {
14170
+ if (!tokenIsQuoted(d, d[1] ?? "")) {
14171
+ const outputMatch = /(?:^|\s)of=("[^"]*"|'[^']*'|[^\s]+)/i.exec(d[2] ?? "");
14172
+ const output = outputMatch?.[1]?.replace(/^['"]|['"]$/g, "");
14173
+ if (output) targets.push(output);
14174
+ }
14175
+ d = dd.exec(normalizedCommand);
14176
+ }
14177
+ const overwrite = /(?:^|[;&|\r\n]\s*)(?:sudo\s+)?(sed|ln)\s+([^;&|\r\n]+)/gi;
14178
+ let o = overwrite.exec(normalizedCommand);
14179
+ while (o !== null) {
14180
+ const rawArgs = o[2] ?? "";
14181
+ const tool = o[1]?.toLowerCase();
14182
+ if (tool && tokenIsQuoted(o, tool)) {
14183
+ o = overwrite.exec(normalizedCommand);
14184
+ continue;
14185
+ }
14186
+ if (tool === "sed" && /(?:^|\s)-i(?:[^\s]*)?(?:\s|$)/.test(rawArgs)) {
14187
+ const args = shellArgs(rawArgs);
14188
+ targets.push(...args.slice(1));
14189
+ } else if (tool === "ln" && /(?:^|\s)-[^\s]*f[^\s]*(?:\s|$)/.test(rawArgs)) {
14190
+ const destination = shellArgs(rawArgs).at(-1);
14191
+ if (destination) targets.push(destination);
14192
+ }
14193
+ o = overwrite.exec(normalizedCommand);
14194
+ }
14195
+ const xargsPipeline = /\b(?:echo|printf)\s+([^|]+)\|\s*xargs(?:\s+-[^\s]+)*\s+(?:sudo\s+)?(?:rm|rmdir|del|unlink|truncate|shred)\b/gi;
14196
+ let x = xargsPipeline.exec(normalizedCommand);
14197
+ while (x !== null) {
14198
+ if (!tokenIsQuoted(x, "xargs")) targets.push(...shellArgs(x[1] ?? ""));
14199
+ x = xargsPipeline.exec(normalizedCommand);
14200
+ }
14201
+ for (const rawArguments of gitInvocationArguments(normalizedCommand)) {
14202
+ const invocationTokens = shellOperandTokens(rawArguments);
14203
+ const commandIndex = gitSubcommandIndex(invocationTokens);
14204
+ if (commandIndex >= 0) {
14205
+ let gitCwd = "";
14206
+ let workTree;
14207
+ for (let index = 0; index < commandIndex; index += 1) {
14208
+ const token = invocationTokens[index] ?? "";
14209
+ if (token === "--") continue;
14210
+ const optionName = token.startsWith("-C") && token !== "-C" ? "-C" : token.split("=", 1)[0] ?? token;
14211
+ let optionValue = token.includes("=") ? token.slice(token.indexOf("=") + 1) : void 0;
14212
+ if (token.startsWith("-C") && token !== "-C") optionValue = token.slice(2);
14213
+ if (VALUE_TAKING_GIT_OPTIONS.has(optionName) && optionValue === void 0) {
14214
+ optionValue = invocationTokens[index + 1];
14215
+ index += 1;
14216
+ }
14217
+ if (optionName === "-C" && optionValue)
14218
+ gitCwd = resolveTargetPath(optionValue, gitCwd || void 0);
14219
+ if (optionName === "--work-tree" && optionValue) workTree = optionValue;
14220
+ }
14221
+ const subcommand = invocationTokens[commandIndex]?.toLowerCase();
14222
+ const tokens = invocationTokens.slice(commandIndex + 1);
14223
+ const gitTreeRoot = workTree ? resolveTargetPath(workTree, gitCwd || void 0) : gitCwd;
14224
+ const gitTarget = (target) => {
14225
+ const resolved = resolveTargetPath(target, gitTreeRoot || void 0);
14226
+ return target.endsWith("/") && !resolved.endsWith("/") ? `${resolved}/` : resolved;
14227
+ };
14228
+ const gitPathspecTargets = (pathspec) => {
14229
+ if (isUnresolvedPathScope(pathspec) || !isDirectoryAmbiguousPath(pathspec)) {
14230
+ return [gitTarget(pathspec)];
14231
+ }
14232
+ return [gitTarget(`${pathspec.replace(/\/$/, "")}/**`)];
14233
+ };
14234
+ const fileSourcedPathspecScope = (pathspecTokens) => {
14235
+ const usesPathspecFile = pathspecTokens.some(
14236
+ (token, index) => token.startsWith("--pathspec-from-file=") || token === "--pathspec-from-file" && pathspecTokens[index + 1] !== void 0
14237
+ );
14238
+ return usesPathspecFile ? gitTarget("**") : void 0;
14239
+ };
14240
+ if (subcommand === "clean") {
14241
+ const dryRun = tokens.some((t2) => t2 === "--dry-run" || /^-[^-]*n/.test(t2));
14242
+ if (!dryRun) {
14243
+ const operands = [];
14244
+ for (let i = 0; i < tokens.length; i += 1) {
14245
+ const t2 = tokens[i] ?? "";
14246
+ if (t2 === "-e" || t2 === "--exclude" || t2 === "--exclude-from") {
14247
+ i += 1;
14248
+ continue;
14249
+ }
14250
+ if (t2.startsWith("--exclude=") || t2.startsWith("--exclude-from=") || /^-e.+/.test(t2) || t2.startsWith("-"))
14251
+ continue;
14252
+ operands.push(t2);
14253
+ }
14254
+ targets.push(
14255
+ ...(operands.length ? operands : ["."]).map(
14256
+ (operand) => gitTarget(operand.endsWith("/") ? `${operand}**` : operand)
14257
+ )
14258
+ );
14259
+ }
14260
+ } else if (subcommand === "rm") {
14261
+ const writes = !tokens.includes("--cached") || tokens.includes("--worktree");
14262
+ if (writes) {
14263
+ const recursive = tokens.some((t2) => t2 === "--recursive" || /^-[^-]*r/.test(t2));
14264
+ const operands = tokens.filter((t2) => t2 !== "--" && !t2.startsWith("-"));
14265
+ targets.push(...operands.map((o2) => gitTarget(recursive ? `${o2}/**` : o2)));
14266
+ const unresolvedFileScope = fileSourcedPathspecScope(tokens);
14267
+ if (unresolvedFileScope) targets.push(unresolvedFileScope);
14268
+ }
14269
+ } else if (subcommand === "restore") {
14270
+ const staged = tokens.includes("--staged") || tokens.some((t2) => /^-[^-]*S/.test(t2));
14271
+ const worktree = tokens.includes("--worktree") || tokens.some((t2) => /^-[^-]*W/.test(t2));
14272
+ if (!staged || worktree) {
14273
+ const operands = [];
14274
+ for (let index = 0; index < tokens.length; index += 1) {
14275
+ const token = tokens[index] ?? "";
14276
+ if (token === "--source" || token === "-s") {
14277
+ index += 1;
14278
+ continue;
14279
+ }
14280
+ if (token.startsWith("--source=") || /^-s.+/.test(token) && token !== "--staged") {
14281
+ continue;
14282
+ }
14283
+ if (token !== "--" && !token.startsWith("-")) operands.push(token);
14284
+ }
14285
+ targets.push(...operands.flatMap(gitPathspecTargets));
14286
+ const unresolvedFileScope = fileSourcedPathspecScope(tokens);
14287
+ if (unresolvedFileScope) targets.push(unresolvedFileScope);
14288
+ }
14289
+ } else if (subcommand === "checkout" || subcommand === "switch") {
14290
+ const separator = tokens.indexOf("--");
14291
+ if (separator >= 0) {
14292
+ const paths = tokens.slice(separator + 1);
14293
+ targets.push(...paths.flatMap(gitPathspecTargets));
14294
+ } else {
14295
+ const createFlags = subcommand === "checkout" ? /* @__PURE__ */ new Set(["-b", "-B", "--branch", "--orphan"]) : /* @__PURE__ */ new Set(["-c", "-C", "--create", "--force-create"]);
14296
+ const createIndex = tokens.findIndex((token) => createFlags.has(token));
14297
+ const branchNameIndex = createIndex >= 0 ? createIndex + 1 : -1;
14298
+ const operands = tokens.filter(
14299
+ (token, index) => !token.startsWith("-") && index !== branchNameIndex
14300
+ );
14301
+ if (operands.length > 0) targets.push(gitTarget("."));
14302
+ }
14303
+ } else if (subcommand === "stash") {
14304
+ const action = tokens.find((token) => !token.startsWith("-"))?.toLowerCase();
14305
+ if (action === void 0 || /^(?:push|save|pop|apply)$/.test(action)) {
14306
+ targets.push(gitTarget("."));
14307
+ }
14308
+ } else if (subcommand === "reset" && tokens.some((t2) => t2 === "--hard" || t2 === "--merge" || t2 === "--keep"))
14309
+ targets.push(gitTarget("."));
14310
+ }
14311
+ }
14312
+ const findDelete = /(?:^|[;&|\r\n]\s*|\$\(\s*|\(\s*|`\s*)find\b([^;&|)`]*(?:\s-delete(?:\s|$)|\s-exec(?:dir)?\s+(?:[^\s;&|]+[\\/])?(?:rm|rmdir)\b)[^;&|)`]*)/gi;
14313
+ let f = findDelete.exec(normalizedCommand);
14314
+ while (f !== null) {
14315
+ const tokens = shellTokens(f[1] ?? "");
14316
+ const expressionIndex = tokens.findIndex(
14317
+ (token) => token.startsWith("-") || token === "!" || token === "("
14318
+ );
14319
+ const roots = (expressionIndex === -1 ? tokens : tokens.slice(0, expressionIndex)).filter(
14320
+ (token) => token.length > 0
14321
+ );
14322
+ targets.push(...roots.length > 0 ? roots : ["."]);
14323
+ f = findDelete.exec(normalizedCommand);
14324
+ }
14325
+ const shellWrapper = /\b(?:ba|z|k)?sh\s+-c\s+(['"])(.*?)\1/gi;
14326
+ let w = shellWrapper.exec(normalizedCommand);
14327
+ while (w !== null) {
14328
+ if (w[2] && !tokenIsQuoted(w, w[0].split(/\s/)[0] ?? "")) {
14329
+ targets.push(...destructiveTargetsAtDepth(w[2], depth + 1));
14330
+ }
14331
+ w = shellWrapper.exec(normalizedCommand);
14332
+ }
14333
+ let quote = null;
14334
+ for (let index = 0; index < normalizedCommand.length; index += 1) {
14335
+ const char = normalizedCommand[index];
14336
+ if (char === "\n") {
14337
+ quote = null;
14338
+ continue;
14339
+ }
14340
+ if (isQuoteBoundary(normalizedCommand, index, quote)) {
14341
+ quote = quote === char ? null : char === "'" ? "'" : '"';
14342
+ continue;
14343
+ }
14344
+ if (quote !== null || char !== ">") continue;
14345
+ const redirectsBothStreams = normalizedCommand[index + 1] === "&";
14346
+ const overridesNoclobber = normalizedCommand[index + 1] === "|";
14347
+ if (redirectsBothStreams || overridesNoclobber || normalizedCommand[index + 1] === ">") {
14348
+ index += 1;
14349
+ }
14350
+ while (normalizedCommand[index + 1] === " " || normalizedCommand[index + 1] === " ") {
14351
+ index += 1;
14352
+ }
14353
+ const targetQuote = normalizedCommand[index + 1];
14354
+ let end = index + 1;
14355
+ let target;
14356
+ if (targetQuote === "'" || targetQuote === '"') {
14357
+ end = normalizedCommand.indexOf(targetQuote, index + 2);
14358
+ if (end === -1) continue;
14359
+ target = normalizedCommand.slice(index + 2, end);
14360
+ } else {
14361
+ while (end < normalizedCommand.length && !/[\s;&|>()]/.test(normalizedCommand[end] ?? "")) {
14362
+ end += 1;
14363
+ }
14364
+ target = normalizedCommand.slice(index + 1, end);
14365
+ }
14366
+ if (target && !(redirectsBothStreams && (/^\d+$/.test(target) || target === "-"))) {
13208
14367
  targets.push(target);
13209
14368
  }
13210
- r = redirect.exec(command);
14369
+ index = end;
13211
14370
  }
13212
- return targets.map((t) => t.replace(/^['"]|['"]$/g, ""));
14371
+ return [
14372
+ ...new Set(
14373
+ targets.map((target) => target.replace(/^['"]|['"]$/g, "")).filter((target) => target !== "/dev/null" && target.toLowerCase() !== "nul")
14374
+ )
14375
+ ];
13213
14376
  }
13214
14377
  var plugin38 = {
13215
14378
  name: "path-guard",
@@ -13231,7 +14394,7 @@ var plugin38 = {
13231
14394
  protect: {
13232
14395
  type: "array",
13233
14396
  items: { type: "string" },
13234
- description: "Glob patterns for protected paths. Replaces the default set when present."
14397
+ description: "Glob patterns for protected paths. Replaces the default set when present. Writer globs whose unresolved scope overlaps protection are blocked; narrow the target or add an `allow` glob."
13235
14398
  },
13236
14399
  allow: {
13237
14400
  type: "array",
@@ -13256,21 +14419,23 @@ var plugin38 = {
13256
14419
  const cfg = readConfig33(api.config.extensions?.["path-guard"]);
13257
14420
  const protectRes = cfg.protect.map(compilePathGlob);
13258
14421
  const allowRes = cfg.allow.map(compilePathGlob);
13259
- const verdict = (path, tool, operation) => {
14422
+ const verdict = (path, tool, operation, isScope = false) => {
14423
+ const subject = isScope ? `write scope "${path}" may include a protected path \u2014 narrow it or add an \`allow\` glob` : `"${path}" is a protected path`;
14424
+ const matchContext = isScope ? 'its unresolved scope overlaps config.extensions["path-guard"].protect' : 'matched by config.extensions["path-guard"].protect';
13260
14425
  if (cfg.mode === "block") {
13261
14426
  state35.blocks += 1;
13262
14427
  state35.lastBlock = { path, tool, when: (/* @__PURE__ */ new Date()).toISOString() };
13263
14428
  api.metrics.counter("blocks");
13264
14429
  return {
13265
14430
  decision: "block",
13266
- reason: `path-guard: "${path}" is a protected path (matched by config.extensions["path-guard"].protect) \u2014 ${operation} refused. If this change is intentional, ask the user to do it, add an \`allow\` glob, or set mode: "warn".`
14431
+ reason: `path-guard: ${subject} (${matchContext}) \u2014 ${operation} refused. If this change is intentional, ask the user to do it, add an \`allow\` glob, or set mode: "warn".`
13267
14432
  };
13268
14433
  }
13269
14434
  state35.warns += 1;
13270
14435
  api.metrics.counter("warns");
13271
14436
  return {
13272
14437
  decision: "allow",
13273
- additionalContext: `path-guard (warn mode): "${path}" is a protected path and this ${operation} would modify it. Double-check this is intentional.`
14438
+ additionalContext: `path-guard (warn mode): ${subject} and this ${operation} would modify it. Double-check this is intentional.`
13274
14439
  };
13275
14440
  };
13276
14441
  const hook = (input) => {
@@ -13278,28 +14443,56 @@ var plugin38 = {
13278
14443
  state35.invocations += 1;
13279
14444
  const toolName = input.toolName ?? "";
13280
14445
  const ti = input.toolInput ?? {};
13281
- if (toolName === "write" || toolName === "edit") {
13282
- const raw = ti["path"] ?? ti["file_path"] ?? ti["filePath"];
13283
- if (typeof raw !== "string" || raw.length === 0) return;
13284
- if (matchesAny(raw, allowRes)) return;
13285
- if (matchesAny(raw, protectRes)) {
13286
- return verdict(raw, toolName, toolName === "write" ? "write" : "edit");
13287
- }
13288
- return;
13289
- }
13290
- if (toolName === "bash" || toolName === "exec") {
13291
- const command = typeof ti["command"] === "string" ? ti["command"] : "";
13292
- if (!command) return;
13293
- for (const target of destructiveTargets(command)) {
13294
- if (matchesAny(target, allowRes)) continue;
13295
- if (matchesAny(target, protectRes)) {
13296
- return verdict(target, toolName, "destructive shell command");
14446
+ const command = typeof ti["command"] === "string" ? ti["command"] : "";
14447
+ const commandArgs = Array.isArray(ti["args"]) ? ti["args"].filter((arg) => typeof arg === "string") : [];
14448
+ const commandForInspection = [
14449
+ command,
14450
+ ...commandArgs.map((arg) => /^[\w./:@%+=,-]+$/.test(arg) ? arg : JSON.stringify(arg))
14451
+ ].join(" ");
14452
+ if (command && executesShell({
14453
+ toolName: input.toolName,
14454
+ toolInput: ti,
14455
+ toolCapabilities: input.toolCapabilities,
14456
+ toolMutating: input.toolMutating
14457
+ })) {
14458
+ const shellTargets = destructiveTargets(commandForInspection);
14459
+ const effectiveCwd = effectiveToolCwd(ti["cwd"], input.cwd);
14460
+ const recursivelyDeletes = commandRecursivelyDeletes(commandForInspection);
14461
+ const deletesImplicitScope = commandDeletesImplicitScope(commandForInspection);
14462
+ for (const path of shellTargets) {
14463
+ const target = {
14464
+ path: relativeToInvocationCwd(resolveTargetPath(path, effectiveCwd), input.cwd),
14465
+ kind: isRootPathScope(path) && deletesImplicitScope || isUnresolvedPathScope(path) || recursivelyDeletes && (isDirectoryAmbiguousPath(path) || hasConfiguredProtectedDescendant(path, cfg.protect)) ? "deletion-scope" : "file"
14466
+ };
14467
+ if (targetFullyAllowed(target, cfg.allow, allowRes)) continue;
14468
+ const protectedShellTarget = targetIntersectsPatterns(target, cfg.protect, protectRes) || matchesAny(`${target.path.replace(/\/$/, "")}/.path-guard-probe`, protectRes);
14469
+ if (protectedShellTarget) {
14470
+ return verdict(
14471
+ target.path,
14472
+ toolName,
14473
+ "destructive shell command",
14474
+ target.kind !== "file"
14475
+ );
13297
14476
  }
13298
14477
  }
14478
+ const writes = writesToDisk({ ...input, toolInput: ti });
14479
+ const hasStructuredTarget = [...PATH_FIELDS, ...PATH_LIST_FIELDS].some((field) => ti[field] !== void 0) || typeof ti["patch"] === "string";
14480
+ const needsImplicitWriteScope = writes && ((input.toolCapabilities?.some(
14481
+ (capability) => DISK_MUTATING_CAPABILITIES.has(capability)
14482
+ ) ?? false) || (!input.toolCapabilities || input.toolCapabilities.length === 0) && LEGACY_WRITE_TOOLS.has(toolName));
14483
+ if (!writes || !hasStructuredTarget && !needsImplicitWriteScope) return;
14484
+ }
14485
+ if (!writesToDisk({ ...input, toolInput: ti }) || isReadOnlyInvocation(toolName, ti)) return;
14486
+ const targets = pathsFromToolInput(ti, toolName, input.cwd);
14487
+ for (const target of targets) {
14488
+ if (targetFullyAllowed(target, cfg.allow, allowRes)) continue;
14489
+ if (targetIntersectsPatterns(target, cfg.protect, protectRes)) {
14490
+ return verdict(target.path, toolName, operationLabel(toolName), target.kind !== "file");
14491
+ }
13299
14492
  }
13300
14493
  return;
13301
14494
  };
13302
- state35.hookUnregister = api.registerHook("PreToolUse", "write|edit|bash|exec", hook, {
14495
+ state35.hookUnregister = api.registerHook("PreToolUse", "*", hook, {
13303
14496
  name: "path-guard",
13304
14497
  stage: "validate",
13305
14498
  failurePolicy: "closed",
@@ -14064,9 +15257,15 @@ var plugin40 = {
14064
15257
  }
14065
15258
  }
14066
15259
  },
14067
- permission: "auto",
15260
+ // Writes the draft to disk (`writeFile` below), so `mutating: false` was
15261
+ // not just a missing capability — it actively LIED, letting even a gate
15262
+ // that reads `mutating` correctly through. It also flipped the tool onto
15263
+ // the wrong side of `smoke.test.ts`, which skips `mutating: true` tools:
15264
+ // this one was exercised by the suite and wrote to the repo during it.
15265
+ permission: "confirm",
14068
15266
  category: "Workflow",
14069
- mutating: false,
15267
+ mutating: true,
15268
+ capabilities: ["fs.write"],
14070
15269
  async execute(input) {
14071
15270
  if (!cfg.enabled) return { ok: false, error: "pr-drafter is disabled" };
14072
15271
  const draft = await buildDraft(cfg, api.llm);
@@ -18112,6 +19311,7 @@ import { readdir as readdir5 } from "node:fs/promises";
18112
19311
  import { isAbsolute as isAbsolute24, join as join6, relative as relative24, resolve as resolve24 } from "node:path";
18113
19312
  var API_VERSION33 = "^0.1.10";
18114
19313
  function withinProject6(p) {
19314
+ if (p.startsWith("-")) return false;
18115
19315
  const root = process.cwd();
18116
19316
  const resolved = isAbsolute24(p) ? resolve24(p) : resolve24(root, p);
18117
19317
  const rel = relative24(root, resolved);
@@ -18501,9 +19701,13 @@ var plugin52 = {
18501
19701
  },
18502
19702
  required: ["path", "oldName", "newName"]
18503
19703
  },
18504
- permission: "auto",
19704
+ // Rewrites source files on `apply: true`. `auto` + no declared
19705
+ // capability meant no confirmation prompt and no read-only-mode block —
19706
+ // `readonly-permission-policy` keys on `capabilities`, not `mutating`.
19707
+ permission: "confirm",
18505
19708
  category: "Development",
18506
19709
  mutating: true,
19710
+ capabilities: ["fs.write"],
18507
19711
  async execute(input) {
18508
19712
  if (!cfg.enabled) return { ok: false, error: "smart-rename is disabled" };
18509
19713
  const rawPath = input.path;
@@ -19140,6 +20344,33 @@ function validateRelativeTemplatePath(field, value) {
19140
20344
  }
19141
20345
  return null;
19142
20346
  }
20347
+ var PROTECTED_WRITE_PREFIXES = [
20348
+ ".git/",
20349
+ ".husky/",
20350
+ ".github/workflows/",
20351
+ ".wrongstack/",
20352
+ ".claude/",
20353
+ "node_modules/"
20354
+ ];
20355
+ var PROTECTED_WRITE_FILES = /* @__PURE__ */ new Set([
20356
+ "package.json",
20357
+ "pnpm-workspace.yaml",
20358
+ "package-lock.json",
20359
+ "pnpm-lock.yaml",
20360
+ "yarn.lock",
20361
+ ".npmrc",
20362
+ ".gitattributes"
20363
+ ]);
20364
+ function validateWritableTemplateTarget(field, value) {
20365
+ const baseError = validateRelativeTemplatePath(field, value);
20366
+ if (baseError) return baseError;
20367
+ const norm = value.replace(/\\/g, "/").replace(/^\.\//, "").toLowerCase();
20368
+ const base = norm.slice(norm.lastIndexOf("/") + 1);
20369
+ if (PROTECTED_WRITE_PREFIXES.some((p) => norm.startsWith(p)) || PROTECTED_WRITE_FILES.has(base) || base === ".env" || base.startsWith(".env.")) {
20370
+ return `${field} "${value}" is a protected path (VCS hooks, CI workflows, dependency manifests, or agent configuration). Write it with the \`write\` tool instead, which prompts for confirmation.`;
20371
+ }
20372
+ return null;
20373
+ }
19143
20374
  var plugin54 = {
19144
20375
  name: "template-engine",
19145
20376
  version: "0.1.0",
@@ -19185,7 +20416,14 @@ var plugin54 = {
19185
20416
  },
19186
20417
  required: ["template", "variables"]
19187
20418
  },
19188
- permission: "auto",
20419
+ // Writes caller-supplied content to a caller-supplied path. `auto` +
20420
+ // no declared capability meant no confirmation prompt, no read-only-mode
20421
+ // block (`readonly-permission-policy` keys on `capabilities`, not
20422
+ // `mutating`), and no PreToolUse hook — plugin tools reach the executor
20423
+ // through `plugin_manager action:'use'`, which bypasses it.
20424
+ permission: "confirm",
20425
+ capabilities: ["fs.write"],
20426
+ riskTier: "destructive",
19189
20427
  category: "Project",
19190
20428
  mutating: true,
19191
20429
  async execute(input) {
@@ -19206,9 +20444,13 @@ var plugin54 = {
19206
20444
  return { ok: false, error: String(err) };
19207
20445
  }
19208
20446
  if (output_path) {
19209
- const pathError = validateRelativeTemplatePath("output_path", output_path);
20447
+ const pathError = validateWritableTemplateTarget("output_path", output_path);
19210
20448
  if (pathError) return { ok: false, error: pathError };
19211
- await writeFile5(output_path, result, "utf-8");
20449
+ try {
20450
+ await writeFile5(output_path, result, "utf-8");
20451
+ } catch (err) {
20452
+ return { ok: false, error: `Could not write ${output_path}: ${String(err)}` };
20453
+ }
19212
20454
  return {
19213
20455
  ok: true,
19214
20456
  output_path,
@@ -19244,7 +20486,10 @@ var plugin54 = {
19244
20486
  },
19245
20487
  required: ["template_path", "variables"]
19246
20488
  },
19247
- permission: "auto",
20489
+ // Same reasoning as `template_expand` above.
20490
+ permission: "confirm",
20491
+ capabilities: ["fs.write"],
20492
+ riskTier: "destructive",
19248
20493
  mutating: true,
19249
20494
  async execute(input) {
19250
20495
  const template_path = input["template_path"];
@@ -19272,9 +20517,13 @@ var plugin54 = {
19272
20517
  return { ok: false, error: `Template rendering failed: ${err}` };
19273
20518
  }
19274
20519
  if (output_path) {
19275
- const pathError = validateRelativeTemplatePath("output_path", output_path);
20520
+ const pathError = validateWritableTemplateTarget("output_path", output_path);
19276
20521
  if (pathError) return { ok: false, error: pathError };
19277
- await writeFile5(output_path, result, "utf-8");
20522
+ try {
20523
+ await writeFile5(output_path, result, "utf-8");
20524
+ } catch (err) {
20525
+ return { ok: false, error: `Could not write ${output_path}: ${String(err)}` };
20526
+ }
19278
20527
  return {
19279
20528
  ok: true,
19280
20529
  template_path,
@@ -20085,6 +21334,30 @@ function readConfig50(raw) {
20085
21334
  maxSourceChars: typeof r["maxSourceChars"] === "number" && r["maxSourceChars"] >= 1e3 && r["maxSourceChars"] <= 1e5 ? r["maxSourceChars"] : DEFAULTS47.maxSourceChars
20086
21335
  };
20087
21336
  }
21337
+ var SOURCE_EXTENSIONS = [
21338
+ ".ts",
21339
+ ".tsx",
21340
+ ".js",
21341
+ ".jsx",
21342
+ ".mjs",
21343
+ ".cjs",
21344
+ ".mts",
21345
+ ".cts",
21346
+ ".py",
21347
+ ".go",
21348
+ ".rs",
21349
+ ".java",
21350
+ ".kt",
21351
+ ".rb",
21352
+ ".php",
21353
+ ".cs",
21354
+ ".swift",
21355
+ ".c",
21356
+ ".h",
21357
+ ".cc",
21358
+ ".cpp",
21359
+ ".hpp"
21360
+ ];
20088
21361
  function withinProject9(p) {
20089
21362
  if (typeof p !== "string" || p.length === 0 || p.length > 4096) return false;
20090
21363
  const root = process.cwd();
@@ -20312,6 +21585,12 @@ var plugin57 = {
20312
21585
  if (!withinProject9(rawPath)) {
20313
21586
  return { ok: false, error: "path is outside the project root" };
20314
21587
  }
21588
+ if (!SOURCE_EXTENSIONS.some((ext) => rawPath.toLowerCase().endsWith(ext))) {
21589
+ return {
21590
+ ok: false,
21591
+ error: `test generation only reads source files (${SOURCE_EXTENSIONS.join(", ")}); refusing "${rawPath}"`
21592
+ };
21593
+ }
20315
21594
  const resolved = resolve27(process.cwd(), rawPath);
20316
21595
  state54.generateCount += 1;
20317
21596
  let result;