claude-warden 2.9.0 → 2.10.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.
@@ -8,7 +8,7 @@
8
8
  {
9
9
  "name": "warden",
10
10
  "description": "Auto-approves safe commands, blocks dangerous ones, prompts for the rest",
11
- "version": "2.9.0",
11
+ "version": "2.10.1",
12
12
  "author": {
13
13
  "name": "banyudu"
14
14
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "warden",
3
- "version": "2.9.0",
3
+ "version": "2.10.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/cli.cjs CHANGED
@@ -18214,6 +18214,19 @@ function convertCommand(node) {
18214
18214
  const raw = rawParts.join(" ");
18215
18215
  return { command, originalCommand, args: args2, envPrefixes, raw };
18216
18216
  }
18217
+ function collectExpansionsFromWord(word, result) {
18218
+ if (!word?.expansion) return;
18219
+ for (const exp of word.expansion) {
18220
+ if (exp.type === "CommandExpansion" && exp.command) {
18221
+ result.hasSubshell = true;
18222
+ result.subshellCommands.push(exp.command);
18223
+ }
18224
+ }
18225
+ }
18226
+ function walkCompoundList(list, result) {
18227
+ if (!list?.commands) return;
18228
+ for (const cmd of list.commands) walkNode(cmd, result);
18229
+ }
18217
18230
  function collectCommandExpansions(node) {
18218
18231
  const commands = [];
18219
18232
  if (node.type === "Command") {
@@ -18280,7 +18293,6 @@ function walkNode(node, result) {
18280
18293
  break;
18281
18294
  }
18282
18295
  case "Subshell": {
18283
- result.hasSubshell = true;
18284
18296
  const subshell = node;
18285
18297
  if (subshell.list?.commands) {
18286
18298
  for (const cmd of subshell.list.commands) {
@@ -18289,15 +18301,47 @@ function walkNode(node, result) {
18289
18301
  }
18290
18302
  break;
18291
18303
  }
18292
- // Complex constructs flag as subshell for safety
18293
- case "If":
18294
- case "For":
18304
+ // Control constructs mirror the Subshell handler above: their bodies are
18305
+ // walked into result.commands so each inner command is evaluated normally.
18306
+ // hasSubshell stays false unless a head expression (`for f in $(...)`,
18307
+ // `case $(...) in ...`) introduces a real command substitution.
18308
+ case "For": {
18309
+ const f = node;
18310
+ if (f.wordlist) {
18311
+ for (const w of f.wordlist) collectExpansionsFromWord(w, result);
18312
+ }
18313
+ walkCompoundList(f.do, result);
18314
+ break;
18315
+ }
18295
18316
  case "While":
18296
- case "Until":
18297
- case "Case":
18298
- case "Function":
18299
- result.hasSubshell = true;
18317
+ case "Until": {
18318
+ const w = node;
18319
+ walkCompoundList(w.clause, result);
18320
+ walkCompoundList(w.do, result);
18321
+ break;
18322
+ }
18323
+ case "If": {
18324
+ const i = node;
18325
+ walkCompoundList(i.clause, result);
18326
+ walkCompoundList(i.then, result);
18327
+ if (i.else) {
18328
+ if (i.else.type === "If") walkNode(i.else, result);
18329
+ else walkCompoundList(i.else, result);
18330
+ }
18300
18331
  break;
18332
+ }
18333
+ case "Case": {
18334
+ const cs = node;
18335
+ collectExpansionsFromWord(cs.clause, result);
18336
+ if (cs.cases) {
18337
+ for (const item of cs.cases) walkCompoundList(item.body, result);
18338
+ }
18339
+ break;
18340
+ }
18341
+ case "Function": {
18342
+ walkCompoundList(node.body, result);
18343
+ break;
18344
+ }
18301
18345
  default:
18302
18346
  break;
18303
18347
  }
@@ -19519,7 +19563,18 @@ var DEFAULT_CONFIG = {
19519
19563
  })),
19520
19564
  // --- Scripting languages ---
19521
19565
  { command: "ruby", default: "ask", argPatterns: [...inlineExecPatterns("Ruby", ["^-e$", "^--eval"]), VERSION_HELP_FLAGS] },
19522
- { command: "perl", default: "ask", argPatterns: [...inlineExecPatterns("Perl", ["^-e$", "^-E$"]), VERSION_HELP_FLAGS] },
19566
+ // `[npa]` bundles perl's common read-only short flags (`-pe`, `-ne`, `-ane`).
19567
+ // `-i` (in-place edit) mutates files — detected separately so it's caught whether
19568
+ // bundled (`-pie`, `-pi`) or passed as its own arg (`-i -pe`, `-i.bak -pe`).
19569
+ {
19570
+ command: "perl",
19571
+ default: "ask",
19572
+ argPatterns: [
19573
+ { match: { anyArgMatches: ["^-[a-z]*i"] }, decision: "ask", reason: "Perl `-i` does in-place file edits. Save the script to scripts/*.pl and run it." },
19574
+ ...inlineExecPatterns("Perl", ["^-[npa]*[eE]$"]),
19575
+ VERSION_HELP_FLAGS
19576
+ ]
19577
+ },
19523
19578
  { command: "php", default: "ask", argPatterns: [...inlineExecPatterns("PHP", ["^-r$"]), VERSION_HELP_FLAGS] },
19524
19579
  // --- Java ecosystem ---
19525
19580
  { command: "java", default: "ask", argPatterns: [VERSION_HELP_FLAGS] },
@@ -18218,6 +18218,19 @@ function convertCommand(node) {
18218
18218
  const raw = rawParts.join(" ");
18219
18219
  return { command, originalCommand, args: args2, envPrefixes, raw };
18220
18220
  }
18221
+ function collectExpansionsFromWord(word, result) {
18222
+ if (!word?.expansion) return;
18223
+ for (const exp of word.expansion) {
18224
+ if (exp.type === "CommandExpansion" && exp.command) {
18225
+ result.hasSubshell = true;
18226
+ result.subshellCommands.push(exp.command);
18227
+ }
18228
+ }
18229
+ }
18230
+ function walkCompoundList(list, result) {
18231
+ if (!list?.commands) return;
18232
+ for (const cmd of list.commands) walkNode(cmd, result);
18233
+ }
18221
18234
  function collectCommandExpansions(node) {
18222
18235
  const commands = [];
18223
18236
  if (node.type === "Command") {
@@ -18284,7 +18297,6 @@ function walkNode(node, result) {
18284
18297
  break;
18285
18298
  }
18286
18299
  case "Subshell": {
18287
- result.hasSubshell = true;
18288
18300
  const subshell = node;
18289
18301
  if (subshell.list?.commands) {
18290
18302
  for (const cmd of subshell.list.commands) {
@@ -18293,15 +18305,47 @@ function walkNode(node, result) {
18293
18305
  }
18294
18306
  break;
18295
18307
  }
18296
- // Complex constructs flag as subshell for safety
18297
- case "If":
18298
- case "For":
18308
+ // Control constructs mirror the Subshell handler above: their bodies are
18309
+ // walked into result.commands so each inner command is evaluated normally.
18310
+ // hasSubshell stays false unless a head expression (`for f in $(...)`,
18311
+ // `case $(...) in ...`) introduces a real command substitution.
18312
+ case "For": {
18313
+ const f = node;
18314
+ if (f.wordlist) {
18315
+ for (const w of f.wordlist) collectExpansionsFromWord(w, result);
18316
+ }
18317
+ walkCompoundList(f.do, result);
18318
+ break;
18319
+ }
18299
18320
  case "While":
18300
- case "Until":
18301
- case "Case":
18302
- case "Function":
18303
- result.hasSubshell = true;
18321
+ case "Until": {
18322
+ const w = node;
18323
+ walkCompoundList(w.clause, result);
18324
+ walkCompoundList(w.do, result);
18325
+ break;
18326
+ }
18327
+ case "If": {
18328
+ const i = node;
18329
+ walkCompoundList(i.clause, result);
18330
+ walkCompoundList(i.then, result);
18331
+ if (i.else) {
18332
+ if (i.else.type === "If") walkNode(i.else, result);
18333
+ else walkCompoundList(i.else, result);
18334
+ }
18304
18335
  break;
18336
+ }
18337
+ case "Case": {
18338
+ const cs = node;
18339
+ collectExpansionsFromWord(cs.clause, result);
18340
+ if (cs.cases) {
18341
+ for (const item of cs.cases) walkCompoundList(item.body, result);
18342
+ }
18343
+ break;
18344
+ }
18345
+ case "Function": {
18346
+ walkCompoundList(node.body, result);
18347
+ break;
18348
+ }
18305
18349
  default:
18306
18350
  break;
18307
18351
  }
@@ -19523,7 +19567,18 @@ var DEFAULT_CONFIG = {
19523
19567
  })),
19524
19568
  // --- Scripting languages ---
19525
19569
  { command: "ruby", default: "ask", argPatterns: [...inlineExecPatterns("Ruby", ["^-e$", "^--eval"]), VERSION_HELP_FLAGS] },
19526
- { command: "perl", default: "ask", argPatterns: [...inlineExecPatterns("Perl", ["^-e$", "^-E$"]), VERSION_HELP_FLAGS] },
19570
+ // `[npa]` bundles perl's common read-only short flags (`-pe`, `-ne`, `-ane`).
19571
+ // `-i` (in-place edit) mutates files — detected separately so it's caught whether
19572
+ // bundled (`-pie`, `-pi`) or passed as its own arg (`-i -pe`, `-i.bak -pe`).
19573
+ {
19574
+ command: "perl",
19575
+ default: "ask",
19576
+ argPatterns: [
19577
+ { match: { anyArgMatches: ["^-[a-z]*i"] }, decision: "ask", reason: "Perl `-i` does in-place file edits. Save the script to scripts/*.pl and run it." },
19578
+ ...inlineExecPatterns("Perl", ["^-[npa]*[eE]$"]),
19579
+ VERSION_HELP_FLAGS
19580
+ ]
19581
+ },
19527
19582
  { command: "php", default: "ask", argPatterns: [...inlineExecPatterns("PHP", ["^-r$"]), VERSION_HELP_FLAGS] },
19528
19583
  // --- Java ecosystem ---
19529
19584
  { command: "java", default: "ask", argPatterns: [VERSION_HELP_FLAGS] },
package/dist/copilot.cjs CHANGED
@@ -18214,6 +18214,19 @@ function convertCommand(node) {
18214
18214
  const raw = rawParts.join(" ");
18215
18215
  return { command, originalCommand, args: args2, envPrefixes, raw };
18216
18216
  }
18217
+ function collectExpansionsFromWord(word, result) {
18218
+ if (!word?.expansion) return;
18219
+ for (const exp of word.expansion) {
18220
+ if (exp.type === "CommandExpansion" && exp.command) {
18221
+ result.hasSubshell = true;
18222
+ result.subshellCommands.push(exp.command);
18223
+ }
18224
+ }
18225
+ }
18226
+ function walkCompoundList(list, result) {
18227
+ if (!list?.commands) return;
18228
+ for (const cmd of list.commands) walkNode(cmd, result);
18229
+ }
18217
18230
  function collectCommandExpansions(node) {
18218
18231
  const commands = [];
18219
18232
  if (node.type === "Command") {
@@ -18280,7 +18293,6 @@ function walkNode(node, result) {
18280
18293
  break;
18281
18294
  }
18282
18295
  case "Subshell": {
18283
- result.hasSubshell = true;
18284
18296
  const subshell = node;
18285
18297
  if (subshell.list?.commands) {
18286
18298
  for (const cmd of subshell.list.commands) {
@@ -18289,15 +18301,47 @@ function walkNode(node, result) {
18289
18301
  }
18290
18302
  break;
18291
18303
  }
18292
- // Complex constructs flag as subshell for safety
18293
- case "If":
18294
- case "For":
18304
+ // Control constructs mirror the Subshell handler above: their bodies are
18305
+ // walked into result.commands so each inner command is evaluated normally.
18306
+ // hasSubshell stays false unless a head expression (`for f in $(...)`,
18307
+ // `case $(...) in ...`) introduces a real command substitution.
18308
+ case "For": {
18309
+ const f = node;
18310
+ if (f.wordlist) {
18311
+ for (const w of f.wordlist) collectExpansionsFromWord(w, result);
18312
+ }
18313
+ walkCompoundList(f.do, result);
18314
+ break;
18315
+ }
18295
18316
  case "While":
18296
- case "Until":
18297
- case "Case":
18298
- case "Function":
18299
- result.hasSubshell = true;
18317
+ case "Until": {
18318
+ const w = node;
18319
+ walkCompoundList(w.clause, result);
18320
+ walkCompoundList(w.do, result);
18321
+ break;
18322
+ }
18323
+ case "If": {
18324
+ const i = node;
18325
+ walkCompoundList(i.clause, result);
18326
+ walkCompoundList(i.then, result);
18327
+ if (i.else) {
18328
+ if (i.else.type === "If") walkNode(i.else, result);
18329
+ else walkCompoundList(i.else, result);
18330
+ }
18300
18331
  break;
18332
+ }
18333
+ case "Case": {
18334
+ const cs = node;
18335
+ collectExpansionsFromWord(cs.clause, result);
18336
+ if (cs.cases) {
18337
+ for (const item of cs.cases) walkCompoundList(item.body, result);
18338
+ }
18339
+ break;
18340
+ }
18341
+ case "Function": {
18342
+ walkCompoundList(node.body, result);
18343
+ break;
18344
+ }
18301
18345
  default:
18302
18346
  break;
18303
18347
  }
@@ -19519,7 +19563,18 @@ var DEFAULT_CONFIG = {
19519
19563
  })),
19520
19564
  // --- Scripting languages ---
19521
19565
  { command: "ruby", default: "ask", argPatterns: [...inlineExecPatterns("Ruby", ["^-e$", "^--eval"]), VERSION_HELP_FLAGS] },
19522
- { command: "perl", default: "ask", argPatterns: [...inlineExecPatterns("Perl", ["^-e$", "^-E$"]), VERSION_HELP_FLAGS] },
19566
+ // `[npa]` bundles perl's common read-only short flags (`-pe`, `-ne`, `-ane`).
19567
+ // `-i` (in-place edit) mutates files — detected separately so it's caught whether
19568
+ // bundled (`-pie`, `-pi`) or passed as its own arg (`-i -pe`, `-i.bak -pe`).
19569
+ {
19570
+ command: "perl",
19571
+ default: "ask",
19572
+ argPatterns: [
19573
+ { match: { anyArgMatches: ["^-[a-z]*i"] }, decision: "ask", reason: "Perl `-i` does in-place file edits. Save the script to scripts/*.pl and run it." },
19574
+ ...inlineExecPatterns("Perl", ["^-[npa]*[eE]$"]),
19575
+ VERSION_HELP_FLAGS
19576
+ ]
19577
+ },
19523
19578
  { command: "php", default: "ask", argPatterns: [...inlineExecPatterns("PHP", ["^-r$"]), VERSION_HELP_FLAGS] },
19524
19579
  // --- Java ecosystem ---
19525
19580
  { command: "java", default: "ask", argPatterns: [VERSION_HELP_FLAGS] },
package/dist/index.cjs CHANGED
@@ -18214,6 +18214,19 @@ function convertCommand(node) {
18214
18214
  const raw = rawParts.join(" ");
18215
18215
  return { command, originalCommand, args: args2, envPrefixes, raw };
18216
18216
  }
18217
+ function collectExpansionsFromWord(word, result) {
18218
+ if (!word?.expansion) return;
18219
+ for (const exp of word.expansion) {
18220
+ if (exp.type === "CommandExpansion" && exp.command) {
18221
+ result.hasSubshell = true;
18222
+ result.subshellCommands.push(exp.command);
18223
+ }
18224
+ }
18225
+ }
18226
+ function walkCompoundList(list, result) {
18227
+ if (!list?.commands) return;
18228
+ for (const cmd of list.commands) walkNode(cmd, result);
18229
+ }
18217
18230
  function collectCommandExpansions(node) {
18218
18231
  const commands = [];
18219
18232
  if (node.type === "Command") {
@@ -18280,7 +18293,6 @@ function walkNode(node, result) {
18280
18293
  break;
18281
18294
  }
18282
18295
  case "Subshell": {
18283
- result.hasSubshell = true;
18284
18296
  const subshell = node;
18285
18297
  if (subshell.list?.commands) {
18286
18298
  for (const cmd of subshell.list.commands) {
@@ -18289,15 +18301,47 @@ function walkNode(node, result) {
18289
18301
  }
18290
18302
  break;
18291
18303
  }
18292
- // Complex constructs flag as subshell for safety
18293
- case "If":
18294
- case "For":
18304
+ // Control constructs mirror the Subshell handler above: their bodies are
18305
+ // walked into result.commands so each inner command is evaluated normally.
18306
+ // hasSubshell stays false unless a head expression (`for f in $(...)`,
18307
+ // `case $(...) in ...`) introduces a real command substitution.
18308
+ case "For": {
18309
+ const f = node;
18310
+ if (f.wordlist) {
18311
+ for (const w of f.wordlist) collectExpansionsFromWord(w, result);
18312
+ }
18313
+ walkCompoundList(f.do, result);
18314
+ break;
18315
+ }
18295
18316
  case "While":
18296
- case "Until":
18297
- case "Case":
18298
- case "Function":
18299
- result.hasSubshell = true;
18317
+ case "Until": {
18318
+ const w = node;
18319
+ walkCompoundList(w.clause, result);
18320
+ walkCompoundList(w.do, result);
18321
+ break;
18322
+ }
18323
+ case "If": {
18324
+ const i = node;
18325
+ walkCompoundList(i.clause, result);
18326
+ walkCompoundList(i.then, result);
18327
+ if (i.else) {
18328
+ if (i.else.type === "If") walkNode(i.else, result);
18329
+ else walkCompoundList(i.else, result);
18330
+ }
18300
18331
  break;
18332
+ }
18333
+ case "Case": {
18334
+ const cs = node;
18335
+ collectExpansionsFromWord(cs.clause, result);
18336
+ if (cs.cases) {
18337
+ for (const item of cs.cases) walkCompoundList(item.body, result);
18338
+ }
18339
+ break;
18340
+ }
18341
+ case "Function": {
18342
+ walkCompoundList(node.body, result);
18343
+ break;
18344
+ }
18301
18345
  default:
18302
18346
  break;
18303
18347
  }
@@ -19519,7 +19563,18 @@ var DEFAULT_CONFIG = {
19519
19563
  })),
19520
19564
  // --- Scripting languages ---
19521
19565
  { command: "ruby", default: "ask", argPatterns: [...inlineExecPatterns("Ruby", ["^-e$", "^--eval"]), VERSION_HELP_FLAGS] },
19522
- { command: "perl", default: "ask", argPatterns: [...inlineExecPatterns("Perl", ["^-e$", "^-E$"]), VERSION_HELP_FLAGS] },
19566
+ // `[npa]` bundles perl's common read-only short flags (`-pe`, `-ne`, `-ane`).
19567
+ // `-i` (in-place edit) mutates files — detected separately so it's caught whether
19568
+ // bundled (`-pie`, `-pi`) or passed as its own arg (`-i -pe`, `-i.bak -pe`).
19569
+ {
19570
+ command: "perl",
19571
+ default: "ask",
19572
+ argPatterns: [
19573
+ { match: { anyArgMatches: ["^-[a-z]*i"] }, decision: "ask", reason: "Perl `-i` does in-place file edits. Save the script to scripts/*.pl and run it." },
19574
+ ...inlineExecPatterns("Perl", ["^-[npa]*[eE]$"]),
19575
+ VERSION_HELP_FLAGS
19576
+ ]
19577
+ },
19523
19578
  { command: "php", default: "ask", argPatterns: [...inlineExecPatterns("PHP", ["^-r$"]), VERSION_HELP_FLAGS] },
19524
19579
  // --- Java ecosystem ---
19525
19580
  { command: "java", default: "ask", argPatterns: [VERSION_HELP_FLAGS] },
@@ -20913,11 +20968,12 @@ function generateAllowSnippet(details) {
20913
20968
  }
20914
20969
  return lines.join("\n");
20915
20970
  }
20916
- function formatSystemMessage(decision, rawCommand, details) {
20971
+ function formatSystemMessage(decision, rawCommand, details, fallbackReason) {
20917
20972
  const relevant = details.filter((d) => d.decision !== "allow");
20918
20973
  if (decision === "ask") {
20919
20974
  const parts = relevant.map((d) => `\`${d.command}\`: ${d.reason}`);
20920
- const header = `[warden] ${parts.join(" | ")}`;
20975
+ const body = parts.length > 0 ? parts.join(" | ") : fallbackReason || "";
20976
+ const header = `[warden] ${body}`;
20921
20977
  const subcommandHints = relevant.filter((d) => d.args.length > 0).map((d) => {
20922
20978
  const sub = d.args[0];
20923
20979
  return ` Option A: Allow all \`${d.command}\` \u2192 \`/warden:allow ${d.command}\`
@@ -21191,14 +21247,14 @@ function emitResult(result, label, config) {
21191
21247
  const truncated = label.length > 80 ? label.slice(0, 77) + "..." : label;
21192
21248
  sendNotification("Claude Warden", `Blocked: ${truncated}`, config);
21193
21249
  }
21194
- const msg2 = formatSystemMessage("deny", label, result.details);
21250
+ const msg2 = formatSystemMessage("deny", label, result.details, result.reason);
21195
21251
  emitDecision("deny", msg2, `[warden] Blocked: ${result.reason}`);
21196
21252
  }
21197
21253
  if (config.notifyOnAsk) {
21198
21254
  const truncated = label.length > 80 ? label.slice(0, 77) + "..." : label;
21199
21255
  sendNotification("Claude Warden", `Permission needed: ${truncated}`, config);
21200
21256
  }
21201
- const msg = formatSystemMessage("ask", label, result.details);
21257
+ const msg = formatSystemMessage("ask", label, result.details, result.reason);
21202
21258
  emitDecision("ask", msg);
21203
21259
  }
21204
21260
  main().catch(() => process.exit(0));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-warden",
3
- "version": "2.9.0",
3
+ "version": "2.10.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",