claude-warden 1.7.0 → 1.8.1

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-warden",
3
- "version": "1.7.0",
3
+ "version": "1.8.1",
4
4
  "description": "Smart command safety filter for Claude Code — parses shell pipelines and evaluates per-command safety rules to auto-approve safe commands and block dangerous ones",
5
5
  "author": {
6
6
  "name": "banyudu"
package/dist/index.cjs CHANGED
@@ -18379,12 +18379,92 @@ function parseCommand(input) {
18379
18379
  return { commands: [], hasSubshell: true, subshellCommands: [], parseError: true };
18380
18380
  }
18381
18381
  }
18382
+ const fallback = regexFallbackParse(input);
18383
+ if (fallback) {
18384
+ return { commands: [fallback], hasSubshell: false, subshellCommands: [], parseError: false };
18385
+ }
18382
18386
  return { commands: [], hasSubshell: false, subshellCommands: [], parseError: true };
18383
18387
  }
18384
18388
  }
18389
+ function regexFallbackParse(input) {
18390
+ const trimmed = input.trim();
18391
+ if (!trimmed) return null;
18392
+ let inSingle = false;
18393
+ let inDouble = false;
18394
+ for (let i = 0; i < trimmed.length; i++) {
18395
+ const ch = trimmed[i];
18396
+ if (ch === "\\" && inDouble) {
18397
+ i++;
18398
+ continue;
18399
+ }
18400
+ if (ch === "'" && !inDouble) {
18401
+ inSingle = !inSingle;
18402
+ continue;
18403
+ }
18404
+ if (ch === '"' && !inSingle) {
18405
+ inDouble = !inDouble;
18406
+ continue;
18407
+ }
18408
+ if (!inSingle && !inDouble && (ch === "|" || ch === ";" || ch === "&" && trimmed[i + 1] === "&")) {
18409
+ return null;
18410
+ }
18411
+ }
18412
+ const envPrefixes = [];
18413
+ let rest = trimmed;
18414
+ while (/^[A-Za-z_][A-Za-z0-9_]*=/.test(rest)) {
18415
+ const match = rest.match(/^([A-Za-z_][A-Za-z0-9_]*=\S*)\s*/);
18416
+ if (!match) break;
18417
+ envPrefixes.push(match[1]);
18418
+ rest = rest.slice(match[0].length);
18419
+ }
18420
+ if (!rest) return null;
18421
+ const cmdMatch = rest.match(/^(\S+)/);
18422
+ if (!cmdMatch) return null;
18423
+ const originalCommand = cmdMatch[1];
18424
+ const command = originalCommand.includes("/") ? (0, import_path.basename)(originalCommand) : originalCommand;
18425
+ const argsStr = rest.slice(cmdMatch[0].length).trim();
18426
+ const args2 = [];
18427
+ let current = "";
18428
+ let qSingle = false;
18429
+ let qDouble = false;
18430
+ for (let i = 0; i < argsStr.length; i++) {
18431
+ const ch = argsStr[i];
18432
+ if (ch === "\\" && qDouble && i + 1 < argsStr.length) {
18433
+ current += argsStr[++i];
18434
+ continue;
18435
+ }
18436
+ if (ch === "'" && !qDouble) {
18437
+ qSingle = !qSingle;
18438
+ continue;
18439
+ }
18440
+ if (ch === '"' && !qSingle) {
18441
+ qDouble = !qDouble;
18442
+ continue;
18443
+ }
18444
+ if (!qSingle && !qDouble && /\s/.test(ch)) {
18445
+ if (current) {
18446
+ args2.push(current);
18447
+ current = "";
18448
+ }
18449
+ continue;
18450
+ }
18451
+ current += ch;
18452
+ }
18453
+ if (current) args2.push(current);
18454
+ return { command, originalCommand, args: args2, envPrefixes, raw: trimmed };
18455
+ }
18385
18456
 
18386
18457
  // src/evaluator.ts
18387
18458
  var import_os = require("os");
18459
+ function safeRegexTest(pattern, input) {
18460
+ try {
18461
+ return new RegExp(pattern).test(input);
18462
+ } catch {
18463
+ process.stderr.write(`[warden] Warning: invalid regex pattern: ${pattern}
18464
+ `);
18465
+ return false;
18466
+ }
18467
+ }
18388
18468
  function commandMatchesName(cmd, name) {
18389
18469
  if (name.startsWith("/")) {
18390
18470
  return cmd.originalCommand === name;
@@ -18394,7 +18474,11 @@ function commandMatchesName(cmd, name) {
18394
18474
  }
18395
18475
  return cmd.command === name;
18396
18476
  }
18397
- function evaluate(parsed, config) {
18477
+ var MAX_RECURSION_DEPTH = 10;
18478
+ function evaluate(parsed, config, depth = 0) {
18479
+ if (depth > MAX_RECURSION_DEPTH) {
18480
+ return { decision: "ask", reason: "Maximum recursion depth exceeded", details: [] };
18481
+ }
18398
18482
  if (parsed.parseError) {
18399
18483
  return { decision: "ask", reason: "Could not parse command safely", details: [] };
18400
18484
  }
@@ -18404,7 +18488,7 @@ function evaluate(parsed, config) {
18404
18488
  if (parsed.hasSubshell && parsed.subshellCommands.length > 0) {
18405
18489
  for (const subCmd of parsed.subshellCommands) {
18406
18490
  const subParsed = parseCommand(subCmd);
18407
- const subResult = evaluate(subParsed, config);
18491
+ const subResult = evaluate(subParsed, config, depth + 1);
18408
18492
  if (subResult.decision === "deny") {
18409
18493
  return { decision: "deny", reason: `Subshell command: ${subResult.reason}`, details: subResult.details };
18410
18494
  }
@@ -18417,7 +18501,7 @@ function evaluate(parsed, config) {
18417
18501
  }
18418
18502
  const details = [];
18419
18503
  for (const cmd of parsed.commands) {
18420
- details.push(evaluateCommand(cmd, config));
18504
+ details.push(evaluateCommand(cmd, config, depth));
18421
18505
  }
18422
18506
  const decisions = details.map((d) => d.decision);
18423
18507
  if (decisions.includes("deny")) {
@@ -18438,7 +18522,7 @@ function evaluate(parsed, config) {
18438
18522
  }
18439
18523
  return { decision: "allow", reason: "All commands are safe", details };
18440
18524
  }
18441
- function evaluateCommand(cmd, config) {
18525
+ function evaluateCommand(cmd, config, depth = 0) {
18442
18526
  const { command, args: args2 } = cmd;
18443
18527
  for (const layer of config.layers) {
18444
18528
  if (layer.alwaysDeny.some((name) => commandMatchesName(cmd, name))) {
@@ -18449,28 +18533,52 @@ function evaluateCommand(cmd, config) {
18449
18533
  }
18450
18534
  }
18451
18535
  if ((command === "ssh" || command === "scp" || command === "rsync") && config.trustedSSHHosts?.length) {
18452
- const sshResult = evaluateSSHCommand(cmd, config);
18536
+ const sshResult = evaluateSSHCommand(cmd, config, depth);
18453
18537
  if (sshResult) return sshResult;
18454
18538
  }
18455
18539
  if (command === "docker" && config.trustedDockerContainers?.length) {
18456
- const dockerResult = evaluateDockerExec(cmd, config);
18540
+ const dockerResult = evaluateDockerExec(cmd, config, depth);
18457
18541
  if (dockerResult) return dockerResult;
18458
18542
  }
18459
18543
  if (command === "kubectl" && config.trustedKubectlContexts?.length) {
18460
- const kubectlResult = evaluateKubectlExec(cmd, config);
18544
+ const kubectlResult = evaluateKubectlExec(cmd, config, depth);
18461
18545
  if (kubectlResult) return kubectlResult;
18462
18546
  }
18463
18547
  if (command === "sprite" && config.trustedSprites?.length) {
18464
- const spriteResult = evaluateSpriteExec(cmd, config);
18548
+ const spriteResult = evaluateSpriteExec(cmd, config, depth);
18465
18549
  if (spriteResult) return spriteResult;
18466
18550
  }
18551
+ if (command === "xargs") {
18552
+ return evaluateXargsCommand(cmd, config, depth);
18553
+ }
18554
+ const mergedRule = collectMergedRule(cmd, config);
18555
+ if (mergedRule) {
18556
+ return evaluateRule(cmd, mergedRule);
18557
+ }
18558
+ return { command, args: args2, decision: config.defaultDecision, reason: `No rule for "${command}"`, matchedRule: "default" };
18559
+ }
18560
+ function collectMergedRule(cmd, config) {
18561
+ const matchingRules = [];
18467
18562
  for (const layer of config.layers) {
18468
18563
  const rule = layer.rules.find((r) => commandMatchesName(cmd, r.command));
18469
18564
  if (rule) {
18470
- return evaluateRule(cmd, rule);
18565
+ matchingRules.push(rule);
18566
+ if (rule.override) break;
18471
18567
  }
18472
18568
  }
18473
- return { command, args: args2, decision: config.defaultDecision, reason: `No rule for "${command}"`, matchedRule: "default" };
18569
+ if (matchingRules.length === 0) return null;
18570
+ if (matchingRules.length === 1) return matchingRules[0];
18571
+ const mergedPatterns = [];
18572
+ for (const rule of matchingRules) {
18573
+ if (rule.argPatterns) {
18574
+ mergedPatterns.push(...rule.argPatterns);
18575
+ }
18576
+ }
18577
+ return {
18578
+ command: matchingRules[0].command,
18579
+ default: matchingRules[0].default,
18580
+ argPatterns: mergedPatterns
18581
+ };
18474
18582
  }
18475
18583
  function evaluateRule(cmd, rule) {
18476
18584
  const { command, args: args2 } = cmd;
@@ -18482,10 +18590,10 @@ function evaluateRule(cmd, rule) {
18482
18590
  matched = matched && m.noArgs === (args2.length === 0);
18483
18591
  }
18484
18592
  if (m.argsMatch && matched) {
18485
- matched = m.argsMatch.some((re) => new RegExp(re).test(argsJoined));
18593
+ matched = m.argsMatch.some((re) => safeRegexTest(re, argsJoined));
18486
18594
  }
18487
18595
  if (m.anyArgMatches && matched) {
18488
- matched = args2.some((arg) => m.anyArgMatches.some((re) => new RegExp(re).test(arg)));
18596
+ matched = args2.some((arg) => m.anyArgMatches.some((re) => safeRegexTest(re, arg)));
18489
18597
  }
18490
18598
  if (m.argCount && matched) {
18491
18599
  if (m.argCount.min !== void 0) matched = matched && args2.length >= m.argCount.min;
@@ -18510,6 +18618,126 @@ function evaluateRule(cmd, rule) {
18510
18618
  matchedRule: `${command}:default`
18511
18619
  };
18512
18620
  }
18621
+ var XARGS_SHORT_FLAGS_WITH_VALUE = /* @__PURE__ */ new Set(["E", "I", "L", "n", "P", "s", "S", "d", "a"]);
18622
+ var XARGS_SHORT_FLAGS_NO_VALUE = /* @__PURE__ */ new Set(["0", "e", "o", "p", "r", "t", "x"]);
18623
+ var XARGS_LONG_FLAGS_WITH_VALUE = /* @__PURE__ */ new Set([
18624
+ "--eof",
18625
+ "--replace",
18626
+ "--max-lines",
18627
+ "--max-args",
18628
+ "--max-procs",
18629
+ "--max-chars",
18630
+ "--arg-file",
18631
+ "--delimiter"
18632
+ ]);
18633
+ var XARGS_LONG_FLAGS_NO_VALUE = /* @__PURE__ */ new Set([
18634
+ "--null",
18635
+ "--exit",
18636
+ "--open-tty",
18637
+ "--interactive",
18638
+ "--no-run-if-empty",
18639
+ "--verbose",
18640
+ "--show-limits"
18641
+ ]);
18642
+ function parseXargsSubcommand(args2) {
18643
+ let i = 0;
18644
+ while (i < args2.length) {
18645
+ const arg = args2[i];
18646
+ if (arg === "--") {
18647
+ i++;
18648
+ break;
18649
+ }
18650
+ if (!arg.startsWith("-") || arg === "-") {
18651
+ break;
18652
+ }
18653
+ if (arg.startsWith("--")) {
18654
+ const eqIndex = arg.indexOf("=");
18655
+ const longFlag = eqIndex === -1 ? arg : arg.slice(0, eqIndex);
18656
+ if (XARGS_LONG_FLAGS_WITH_VALUE.has(longFlag)) {
18657
+ if (eqIndex !== -1) {
18658
+ i++;
18659
+ continue;
18660
+ }
18661
+ if (i + 1 >= args2.length) return { subcommand: null, unresolved: true };
18662
+ i += 2;
18663
+ continue;
18664
+ }
18665
+ if (XARGS_LONG_FLAGS_NO_VALUE.has(longFlag)) {
18666
+ i++;
18667
+ continue;
18668
+ }
18669
+ return { subcommand: null, unresolved: true };
18670
+ }
18671
+ const short = arg[1];
18672
+ if (XARGS_SHORT_FLAGS_WITH_VALUE.has(short)) {
18673
+ if (arg.length > 2) {
18674
+ i++;
18675
+ continue;
18676
+ }
18677
+ if (i + 1 >= args2.length) return { subcommand: null, unresolved: true };
18678
+ i += 2;
18679
+ continue;
18680
+ }
18681
+ const grouped = arg.slice(1).split("");
18682
+ const allKnownNoValue = grouped.every((ch) => XARGS_SHORT_FLAGS_NO_VALUE.has(ch));
18683
+ if (allKnownNoValue) {
18684
+ i++;
18685
+ continue;
18686
+ }
18687
+ return { subcommand: null, unresolved: true };
18688
+ }
18689
+ if (i >= args2.length) {
18690
+ return {
18691
+ unresolved: false,
18692
+ subcommand: {
18693
+ command: "echo",
18694
+ originalCommand: "echo",
18695
+ args: [],
18696
+ envPrefixes: [],
18697
+ raw: "echo"
18698
+ }
18699
+ };
18700
+ }
18701
+ const subcommand = args2[i];
18702
+ const subArgs = args2.slice(i + 1);
18703
+ return {
18704
+ unresolved: false,
18705
+ subcommand: {
18706
+ command: subcommand,
18707
+ originalCommand: subcommand,
18708
+ args: subArgs,
18709
+ envPrefixes: [],
18710
+ raw: [subcommand, ...subArgs].join(" ")
18711
+ }
18712
+ };
18713
+ }
18714
+ function evaluateXargsCommand(cmd, config, depth = 0) {
18715
+ const { command, args: args2 } = cmd;
18716
+ const { subcommand, unresolved } = parseXargsSubcommand(args2);
18717
+ if (unresolved || !subcommand) {
18718
+ return {
18719
+ command,
18720
+ args: args2,
18721
+ decision: "ask",
18722
+ reason: "xargs subcommand could not be resolved safely",
18723
+ matchedRule: "xargs:subcommand"
18724
+ };
18725
+ }
18726
+ const parsed = {
18727
+ commands: [subcommand],
18728
+ hasSubshell: false,
18729
+ subshellCommands: [],
18730
+ parseError: false
18731
+ };
18732
+ const result = evaluate(parsed, config, depth + 1);
18733
+ return {
18734
+ command,
18735
+ args: args2,
18736
+ decision: result.decision,
18737
+ reason: `xargs subcommand "${subcommand.command}": ${result.reason}`,
18738
+ matchedRule: "xargs:subcommand"
18739
+ };
18740
+ }
18513
18741
  var SSH_FLAGS_WITH_VALUE = /* @__PURE__ */ new Set([
18514
18742
  "-b",
18515
18743
  "-c",
@@ -18604,7 +18832,7 @@ function parseSSHArgs(args2) {
18604
18832
  }
18605
18833
  return {
18606
18834
  host,
18607
- remoteCommand: remoteArgs.length > 0 ? remoteArgs.join(" ") : null
18835
+ remoteCommand: remoteArgs.length > 0 ? remoteArgs.map(shellQuote).join(" ") : null
18608
18836
  };
18609
18837
  }
18610
18838
  function extractHostFromRemotePath(args2) {
@@ -18614,7 +18842,7 @@ function extractHostFromRemotePath(args2) {
18614
18842
  }
18615
18843
  return null;
18616
18844
  }
18617
- function evaluateSSHCommand(cmd, config) {
18845
+ function evaluateSSHCommand(cmd, config, depth = 0) {
18618
18846
  const { command, args: args2 } = cmd;
18619
18847
  const trustedHosts = config.trustedSSHHosts || [];
18620
18848
  if (command === "scp" || command === "rsync") {
@@ -18622,6 +18850,24 @@ function evaluateSSHCommand(cmd, config) {
18622
18850
  if (!host2) return null;
18623
18851
  const target2 = findMatchingTarget(host2, trustedHosts);
18624
18852
  if (!target2) return null;
18853
+ if (target2.allowAll || !target2.overrides) {
18854
+ return {
18855
+ command,
18856
+ args: args2,
18857
+ decision: "allow",
18858
+ reason: `Trusted SSH host "${host2}"${target2.allowAll ? " (allowAll)" : ""}`,
18859
+ matchedRule: "trustedSSHHosts"
18860
+ };
18861
+ }
18862
+ if (target2.overrides.alwaysDeny.some((name) => name === command)) {
18863
+ return {
18864
+ command,
18865
+ args: args2,
18866
+ decision: "deny",
18867
+ reason: `Trusted SSH host "${host2}": "${command}" blocked by overrides`,
18868
+ matchedRule: "trustedSSHHosts"
18869
+ };
18870
+ }
18625
18871
  return {
18626
18872
  command,
18627
18873
  args: args2,
@@ -18653,7 +18899,7 @@ function evaluateSSHCommand(cmd, config) {
18653
18899
  };
18654
18900
  }
18655
18901
  const parsed = parseCommand(remoteCommand);
18656
- const result = evaluate(parsed, configWithContextOverrides(config, target));
18902
+ const result = evaluate(parsed, configWithContextOverrides(config, target), depth + 1);
18657
18903
  return {
18658
18904
  command,
18659
18905
  args: args2,
@@ -18673,6 +18919,12 @@ var DOCKER_EXEC_FLAGS_WITH_VALUE = /* @__PURE__ */ new Set([
18673
18919
  "--detach-keys"
18674
18920
  ]);
18675
18921
  var INTERACTIVE_SHELLS = /* @__PURE__ */ new Set(["bash", "sh", "zsh"]);
18922
+ function shellQuote(arg) {
18923
+ if (/[\s"'\\$`!#&|;()<>]/.test(arg)) {
18924
+ return `'${arg.replace(/'/g, "'\\''")}'`;
18925
+ }
18926
+ return arg;
18927
+ }
18676
18928
  function configWithContextOverrides(config, target) {
18677
18929
  const overrideLayers = [];
18678
18930
  if (target?.overrides) overrideLayers.push(target.overrides);
@@ -18683,7 +18935,7 @@ function configWithContextOverrides(config, target) {
18683
18935
  layers: [...overrideLayers, ...config.layers]
18684
18936
  };
18685
18937
  }
18686
- function evaluateRemoteCommand(remoteArgs, config, target) {
18938
+ function evaluateRemoteCommand(remoteArgs, config, target, depth = 0) {
18687
18939
  if (target?.allowAll) {
18688
18940
  return { decision: "allow", reason: "allowAll target", details: [] };
18689
18941
  }
@@ -18698,7 +18950,7 @@ function evaluateRemoteCommand(remoteArgs, config, target) {
18698
18950
  if (INTERACTIVE_SHELLS.has(remoteCmd) && remoteArgs[1] === "-c" && remoteArgs.length >= 3) {
18699
18951
  const innerCommand = remoteArgs.slice(2).join(" ");
18700
18952
  const parsed2 = parseCommand(innerCommand);
18701
- return evaluate(parsed2, overriddenConfig);
18953
+ return evaluate(parsed2, overriddenConfig, depth + 1);
18702
18954
  }
18703
18955
  const parsed = {
18704
18956
  commands: [{ command: remoteCmd, originalCommand: remoteCmd, args: remoteArgs.slice(1), envPrefixes: [], raw: remoteArgs.join(" ") }],
@@ -18706,7 +18958,7 @@ function evaluateRemoteCommand(remoteArgs, config, target) {
18706
18958
  subshellCommands: [],
18707
18959
  parseError: false
18708
18960
  };
18709
- return evaluate(parsed, overriddenConfig);
18961
+ return evaluate(parsed, overriddenConfig, depth + 1);
18710
18962
  }
18711
18963
  function parseDockerExecArgs(args2) {
18712
18964
  let target = null;
@@ -18735,14 +18987,14 @@ function parseDockerExecArgs(args2) {
18735
18987
  }
18736
18988
  return { target, remoteArgs };
18737
18989
  }
18738
- function evaluateDockerExec(cmd, config) {
18990
+ function evaluateDockerExec(cmd, config, depth = 0) {
18739
18991
  const { command, args: args2 } = cmd;
18740
18992
  if (args2[0] !== "exec") return null;
18741
18993
  const { target: containerName, remoteArgs } = parseDockerExecArgs(args2.slice(1));
18742
18994
  if (!containerName) return null;
18743
18995
  const matched = findMatchingTarget(containerName, config.trustedDockerContainers || []);
18744
18996
  if (!matched) return null;
18745
- const result = evaluateRemoteCommand(remoteArgs, config, matched);
18997
+ const result = evaluateRemoteCommand(remoteArgs, config, matched, depth);
18746
18998
  return {
18747
18999
  command,
18748
19000
  args: args2,
@@ -18815,14 +19067,14 @@ function parseKubectlExecArgs(args2) {
18815
19067
  }
18816
19068
  return { context, pod, remoteArgs };
18817
19069
  }
18818
- function evaluateKubectlExec(cmd, config) {
19070
+ function evaluateKubectlExec(cmd, config, depth = 0) {
18819
19071
  const { command, args: args2 } = cmd;
18820
19072
  if (args2[0] !== "exec") return null;
18821
19073
  const { context, pod, remoteArgs } = parseKubectlExecArgs(args2.slice(1));
18822
19074
  if (!context) return null;
18823
19075
  const matched = findMatchingTarget(context, config.trustedKubectlContexts || []);
18824
19076
  if (!matched) return null;
18825
- const result = evaluateRemoteCommand(remoteArgs, config, matched);
19077
+ const result = evaluateRemoteCommand(remoteArgs, config, matched, depth);
18826
19078
  return {
18827
19079
  command,
18828
19080
  args: args2,
@@ -18882,13 +19134,13 @@ function parseSpriteExecArgs(args2) {
18882
19134
  }
18883
19135
  return { spriteName, remoteArgs };
18884
19136
  }
18885
- function evaluateSpriteExec(cmd, config) {
19137
+ function evaluateSpriteExec(cmd, config, depth = 0) {
18886
19138
  const { command, args: args2 } = cmd;
18887
19139
  const { spriteName, remoteArgs } = parseSpriteExecArgs(args2);
18888
19140
  if (!spriteName) return null;
18889
19141
  const matched = findMatchingTarget(spriteName, config.trustedSprites || []);
18890
19142
  if (!matched) return null;
18891
- const result = evaluateRemoteCommand(remoteArgs, config, matched);
19143
+ const result = evaluateRemoteCommand(remoteArgs, config, matched, depth);
18892
19144
  return {
18893
19145
  command,
18894
19146
  args: args2,
@@ -19231,6 +19483,7 @@ var DEFAULT_CONFIG = {
19231
19483
  "xcode-select",
19232
19484
  "xcrun",
19233
19485
  "xcodebuild",
19486
+ "networkQuality",
19234
19487
  // Misc safe
19235
19488
  "cd",
19236
19489
  "pushd",
@@ -19339,6 +19592,7 @@ var DEFAULT_CONFIG = {
19339
19592
  // npx / bunx — package runners
19340
19593
  pkgRunnerRule("npx"),
19341
19594
  pkgRunnerRule("bunx"),
19595
+ pkgRunnerRule("pnpx"),
19342
19596
  // npm / pnpm / yarn — package managers
19343
19597
  pkgManagerRule("npm", ["ci", "search", "explain", "prefix", "root", "fund", "doctor", "diff", "pkg", "query", "shrinkwrap"]),
19344
19598
  pkgManagerRule("pnpm", ["store", "fetch", "doctor", "patch"]),
@@ -19592,6 +19846,12 @@ var DEFAULT_CONFIG = {
19592
19846
  { command: "codesign", default: "ask", argPatterns: [
19593
19847
  { match: { anyArgMatches: ["^(-vv|--verify|--display|-d)$"] }, decision: "allow", description: "Verify codesign" }
19594
19848
  ] },
19849
+ { command: "networksetup", default: "ask", argPatterns: [
19850
+ { match: { anyArgMatches: ["^-(get|list|show)"] }, decision: "allow", description: "Read-only network configuration queries" }
19851
+ ] },
19852
+ { command: "scutil", default: "ask", argPatterns: [
19853
+ { match: { anyArgMatches: ["^--get$", "^--dns$", "^--proxy$", "^--nwi$"] }, decision: "allow", description: "Read-only system configuration queries" }
19854
+ ] },
19595
19855
  { command: "osascript", default: "ask" },
19596
19856
  { command: "say", default: "ask" },
19597
19857
  // --- Process management ---
@@ -19604,6 +19864,10 @@ var DEFAULT_CONFIG = {
19604
19864
  };
19605
19865
 
19606
19866
  // src/rules.ts
19867
+ var VALID_DECISIONS = /* @__PURE__ */ new Set(["allow", "deny", "ask"]);
19868
+ function isValidDecision(value) {
19869
+ return VALID_DECISIONS.has(value);
19870
+ }
19607
19871
  var USER_CONFIG_PATHS = [
19608
19872
  (0, import_path2.join)((0, import_os2.homedir)(), ".claude", "warden.yaml"),
19609
19873
  (0, import_path2.join)((0, import_os2.homedir)(), ".claude", "warden.json")
@@ -19654,15 +19918,36 @@ function tryLoadFile(filePath) {
19654
19918
  if (parsed && typeof parsed === "object") {
19655
19919
  return parsed;
19656
19920
  }
19657
- } catch {
19921
+ } catch (err) {
19922
+ process.stderr.write(`[warden] Warning: failed to parse config ${filePath}: ${err instanceof Error ? err.message : String(err)}
19923
+ `);
19658
19924
  }
19659
19925
  return null;
19660
19926
  }
19661
19927
  function extractLayer(raw) {
19928
+ const rules = Array.isArray(raw.rules) ? raw.rules : [];
19929
+ for (const rule of rules) {
19930
+ if (rule && typeof rule === "object") {
19931
+ if (rule.default && !isValidDecision(rule.default)) {
19932
+ process.stderr.write(`[warden] Warning: invalid rule default "${rule.default}" for "${rule.command}", using "ask"
19933
+ `);
19934
+ rule.default = "ask";
19935
+ }
19936
+ if (Array.isArray(rule.argPatterns)) {
19937
+ for (const pattern of rule.argPatterns) {
19938
+ if (pattern?.decision && !isValidDecision(pattern.decision)) {
19939
+ process.stderr.write(`[warden] Warning: invalid pattern decision "${pattern.decision}" for "${rule.command}", using "ask"
19940
+ `);
19941
+ pattern.decision = "ask";
19942
+ }
19943
+ }
19944
+ }
19945
+ }
19946
+ }
19662
19947
  return {
19663
19948
  alwaysAllow: Array.isArray(raw.alwaysAllow) ? raw.alwaysAllow : [],
19664
19949
  alwaysDeny: Array.isArray(raw.alwaysDeny) ? raw.alwaysDeny : [],
19665
- rules: Array.isArray(raw.rules) ? raw.rules : []
19950
+ rules
19666
19951
  };
19667
19952
  }
19668
19953
  function parseTrustedList(raw) {
@@ -19694,7 +19979,12 @@ function mergeNonLayerFields(config, raw) {
19694
19979
  config.trustedSprites = [...config.trustedSprites || [], ...parseTrustedList(raw.trustedSprites)];
19695
19980
  }
19696
19981
  if (typeof raw.defaultDecision === "string") {
19697
- config.defaultDecision = raw.defaultDecision;
19982
+ if (isValidDecision(raw.defaultDecision)) {
19983
+ config.defaultDecision = raw.defaultDecision;
19984
+ } else {
19985
+ process.stderr.write(`[warden] Warning: invalid defaultDecision "${raw.defaultDecision}", ignoring
19986
+ `);
19987
+ }
19698
19988
  }
19699
19989
  if (typeof raw.askOnSubshell === "boolean") {
19700
19990
  config.askOnSubshell = raw.askOnSubshell;
@@ -19850,10 +20140,22 @@ function sendNotification(title, message, config) {
19850
20140
  }
19851
20141
 
19852
20142
  // src/index.ts
20143
+ var MAX_STDIN_SIZE = 1024 * 1024;
19853
20144
  async function main() {
19854
20145
  let raw = "";
19855
20146
  for await (const chunk of process.stdin) {
19856
20147
  raw += chunk;
20148
+ if (raw.length > MAX_STDIN_SIZE) {
20149
+ const output2 = {
20150
+ hookSpecificOutput: {
20151
+ hookEventName: "PreToolUse",
20152
+ permissionDecision: "ask",
20153
+ permissionDecisionReason: "[warden] Input exceeds size limit"
20154
+ }
20155
+ };
20156
+ process.stdout.write(JSON.stringify(output2));
20157
+ process.exit(0);
20158
+ }
19857
20159
  }
19858
20160
  let input;
19859
20161
  try {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-warden",
3
- "version": "1.7.0",
3
+ "version": "1.8.1",
4
4
  "description": "Smart command safety filter for Claude Code — auto-approves safe commands, blocks dangerous ones",
5
5
  "type": "module",
6
6
  "main": "dist/index.cjs",
@@ -28,13 +28,15 @@
28
28
  "README.md",
29
29
  "LICENSE"
30
30
  ],
31
+ "dependencies": {
32
+ "bash-parser": "^0.5.0",
33
+ "yaml": "^2.4.0"
34
+ },
31
35
  "devDependencies": {
32
36
  "@types/node": "^20.0.0",
33
- "bash-parser": "^0.5.0",
34
37
  "tsup": "^8.0.0",
35
38
  "typescript": "^5.4.0",
36
- "vitest": "^1.6.0",
37
- "yaml": "^2.4.0"
39
+ "vitest": "^1.6.0"
38
40
  },
39
41
  "scripts": {
40
42
  "build": "tsup",