claude-warden 1.8.2 → 1.10.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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-warden",
3
- "version": "1.8.2",
3
+ "version": "1.10.0",
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/README.md CHANGED
@@ -133,6 +133,29 @@ rules:
133
133
  description: Read-only docker commands
134
134
  ```
135
135
 
136
+ ## YOLO mode
137
+
138
+ Need to temporarily bypass all permission prompts? YOLO mode auto-allows all commands for a limited time or the full session — while still blocking always-deny commands (like `sudo`, `shutdown`) for safety.
139
+
140
+ ### Activate via slash command
141
+
142
+ ```
143
+ /claude-warden:yolo session # Full session, no expiry
144
+ /claude-warden:yolo 5m # 5 minutes
145
+ /claude-warden:yolo 15m # 15 minutes
146
+ /claude-warden:yolo off # Turn off immediately
147
+ ```
148
+
149
+ Running `/claude-warden:yolo` with no arguments shows a menu of duration options.
150
+
151
+ ### How it works
152
+
153
+ YOLO mode is **session-scoped** — it only affects the current Claude Code session. The hook intercepts special activation commands and stores state in a temp file keyed by session ID. When a command is evaluated during YOLO mode, the hook skips normal rule evaluation and auto-allows (except always-deny commands). Expired YOLO states are cleaned up automatically.
154
+
155
+ ### Discovery
156
+
157
+ When Warden prompts you for permission (`ask` decision), the system message includes a tip about YOLO mode so you can discover it when you need it most.
158
+
136
159
  ## Feedback and `/claude-warden:warden-allow`
137
160
 
138
161
  When Warden blocks or flags a command, it includes a system message explaining:
@@ -86,9 +86,36 @@ notifyOnDeny: true
86
86
 
87
87
  # Command-specific rules (override built-in rules by command name).
88
88
  # The first scope (project > user > default) with a rule for a given command wins.
89
+ #
90
+ # IMPORTANT: Rules are matched by the command being executed, not by arguments.
91
+ # For example, when Claude runs `python -c "import foo"`, warden looks up rules
92
+ # for command: "python" — NOT "bash" or "sh". A common mistake is writing rules
93
+ # for "bash" with argPatterns matching "python"; this won't work because warden
94
+ # sees the command as "python".
95
+ #
89
96
  # rules:
97
+ # # Allow all python commands
98
+ # - command: python
99
+ # default: allow
100
+ #
101
+ # # Allow python -c but ask for other python usage
102
+ # - command: python
103
+ # default: ask
104
+ # argPatterns:
105
+ # - match:
106
+ # anyArgMatches: ['^-c$']
107
+ # decision: allow
108
+ # description: Allow python -c inline scripts
109
+ #
110
+ # # Allow all node.js execution
111
+ # - command: node
112
+ # default: allow
113
+ #
114
+ # # Trust all npx in this project
90
115
  # - command: npx
91
- # default: allow # Trust all npx in this project
116
+ # default: allow
117
+ #
118
+ # # Docker with selective allow
92
119
  # - command: docker
93
120
  # default: ask
94
121
  # argPatterns:
package/dist/index.cjs CHANGED
@@ -18551,6 +18551,9 @@ function evaluateCommand(cmd, config, depth = 0) {
18551
18551
  if (command === "xargs") {
18552
18552
  return evaluateXargsCommand(cmd, config, depth);
18553
18553
  }
18554
+ if (command === "find") {
18555
+ return evaluateFindCommand(cmd, config, depth);
18556
+ }
18554
18557
  const mergedRule = collectMergedRule(cmd, config);
18555
18558
  if (mergedRule) {
18556
18559
  return evaluateRule(cmd, mergedRule);
@@ -18738,6 +18741,64 @@ function evaluateXargsCommand(cmd, config, depth = 0) {
18738
18741
  matchedRule: "xargs:subcommand"
18739
18742
  };
18740
18743
  }
18744
+ function parseFindExecCommands(args2) {
18745
+ const commands = [];
18746
+ let i = 0;
18747
+ while (i < args2.length) {
18748
+ if (args2[i] === "-exec" || args2[i] === "-execdir") {
18749
+ i++;
18750
+ const cmdArgs = [];
18751
+ while (i < args2.length && args2[i] !== ";" && args2[i] !== "+") {
18752
+ if (args2[i] !== "{}") {
18753
+ cmdArgs.push(args2[i]);
18754
+ }
18755
+ i++;
18756
+ }
18757
+ i++;
18758
+ if (cmdArgs.length > 0) {
18759
+ commands.push({
18760
+ command: cmdArgs[0],
18761
+ originalCommand: cmdArgs[0],
18762
+ args: cmdArgs.slice(1),
18763
+ envPrefixes: [],
18764
+ raw: cmdArgs.join(" ")
18765
+ });
18766
+ }
18767
+ } else {
18768
+ i++;
18769
+ }
18770
+ }
18771
+ return commands;
18772
+ }
18773
+ function evaluateFindCommand(cmd, config, depth = 0) {
18774
+ const { command, args: args2 } = cmd;
18775
+ if (args2.some((a) => a === "-delete")) {
18776
+ return { command, args: args2, decision: "ask", reason: "find -delete can remove files", matchedRule: "find:delete" };
18777
+ }
18778
+ if (args2.some((a) => a === "-ok" || a === "-okdir")) {
18779
+ return { command, args: args2, decision: "ask", reason: "find -ok/-okdir can execute commands interactively", matchedRule: "find:ok" };
18780
+ }
18781
+ const execCommands = parseFindExecCommands(args2);
18782
+ if (execCommands.length === 0) {
18783
+ return { command, args: args2, decision: "allow", reason: "find without dangerous flags", matchedRule: "find:safe" };
18784
+ }
18785
+ for (const execCmd of execCommands) {
18786
+ const parsed = {
18787
+ commands: [execCmd],
18788
+ hasSubshell: false,
18789
+ subshellCommands: [],
18790
+ parseError: false
18791
+ };
18792
+ const result = evaluate(parsed, config, depth + 1);
18793
+ if (result.decision === "deny") {
18794
+ return { command, args: args2, decision: "deny", reason: `find -exec: ${result.reason}`, matchedRule: "find:exec" };
18795
+ }
18796
+ if (result.decision === "ask") {
18797
+ return { command, args: args2, decision: "ask", reason: `find -exec: ${result.reason}`, matchedRule: "find:exec" };
18798
+ }
18799
+ }
18800
+ return { command, args: args2, decision: "allow", reason: "find -exec commands are safe", matchedRule: "find:exec" };
18801
+ }
18741
18802
  var SSH_FLAGS_WITH_VALUE = /* @__PURE__ */ new Set([
18742
18803
  "-b",
18743
18804
  "-c",
@@ -19687,13 +19748,7 @@ var DEFAULT_CONFIG = {
19687
19748
  ]
19688
19749
  },
19689
19750
  // --- Potentially dangerous text/file tools ---
19690
- {
19691
- command: "find",
19692
- default: "allow",
19693
- argPatterns: [
19694
- { match: { anyArgMatches: ["^-exec$", "^-execdir$", "^-delete$", "^-ok$", "^-okdir$"] }, decision: "ask", reason: "find can execute or delete files" }
19695
- ]
19696
- },
19751
+ // `find` is handled specially in the evaluator (recursive -exec evaluation)
19697
19752
  {
19698
19753
  command: "sed",
19699
19754
  default: "allow",
@@ -19925,6 +19980,35 @@ function tryLoadFile(filePath) {
19925
19980
  }
19926
19981
  return null;
19927
19982
  }
19983
+ var KNOWN_COMMANDS = /* @__PURE__ */ new Set([
19984
+ ...DEFAULT_CONFIG.layers[0].alwaysAllow,
19985
+ ...DEFAULT_CONFIG.layers[0].alwaysDeny,
19986
+ ...DEFAULT_CONFIG.layers[0].rules.map((r) => r.command)
19987
+ ]);
19988
+ function warnArgPatternCommandMismatch(rule) {
19989
+ if (!Array.isArray(rule.argPatterns)) return;
19990
+ for (const pattern of rule.argPatterns) {
19991
+ const matchers = [
19992
+ ...pattern.match?.anyArgMatches || [],
19993
+ ...pattern.match?.argsMatch || []
19994
+ ];
19995
+ for (const m of matchers) {
19996
+ const literals = extractLiteralsFromPattern(m);
19997
+ for (const lit of literals) {
19998
+ if (lit !== rule.command && KNOWN_COMMANDS.has(lit)) {
19999
+ process.stderr.write(
20000
+ `[warden] Warning: rule for "${rule.command}" has argPattern matching "${lit}" \u2014 this won't work as expected. Rules are matched by the command being run, not its arguments. If you want to control "${lit}", add a separate rule with command: "${lit}".
20001
+ `
20002
+ );
20003
+ }
20004
+ }
20005
+ }
20006
+ }
20007
+ }
20008
+ function extractLiteralsFromPattern(pattern) {
20009
+ let cleaned = pattern.replace(/^\^?\(?(.*?)\)?\$?$/, "$1");
20010
+ return cleaned.split("|").map((s) => s.trim()).filter((s) => /^[a-z][a-z0-9_-]*$/i.test(s));
20011
+ }
19928
20012
  function extractLayer(raw) {
19929
20013
  const rules = Array.isArray(raw.rules) ? raw.rules : [];
19930
20014
  for (const rule of rules) {
@@ -19943,6 +20027,7 @@ function extractLayer(raw) {
19943
20027
  }
19944
20028
  }
19945
20029
  }
20030
+ warnArgPatternCommandMismatch(rule);
19946
20031
  }
19947
20032
  }
19948
20033
  return {
@@ -20053,12 +20138,15 @@ function formatSystemMessage(decision, rawCommand, details) {
20053
20138
  return ` Option A: Allow all \`${d.command}\` \u2192 \`/claude-warden:warden-allow ${d.command}\`
20054
20139
  Option B: Allow only \`${d.command} ${sub}\` \u2192 \`/claude-warden:warden-allow ${d.command} ${sub}\``;
20055
20140
  });
20141
+ const yoloHint = "Tip: `/claude-warden:yolo` to temporarily allow all commands";
20056
20142
  if (subcommandHints.length > 0) {
20057
20143
  return `${header}
20058
20144
  ${subcommandHints.join("\n")}
20059
- See /claude-warden:warden-allow`;
20145
+ See /claude-warden:warden-allow
20146
+ ${yoloHint}`;
20060
20147
  }
20061
- return `${header} \u2014 To auto-allow, see /claude-warden:warden-allow`;
20148
+ return `${header} \u2014 To auto-allow, see /claude-warden:warden-allow
20149
+ ${yoloHint}`;
20062
20150
  }
20063
20151
  const lines = ["[warden] Command blocked", ""];
20064
20152
  if (relevant.length > 0) {
@@ -20140,6 +20228,72 @@ function sendNotification(title, message, config) {
20140
20228
  }
20141
20229
  }
20142
20230
 
20231
+ // src/yolo.ts
20232
+ var import_fs2 = require("fs");
20233
+ var import_os3 = require("os");
20234
+ var import_path3 = require("path");
20235
+ function yoloFilePath(sessionId) {
20236
+ return (0, import_path3.join)((0, import_os3.tmpdir)(), `claude-warden-yolo-${sessionId}`);
20237
+ }
20238
+ function getYoloState(sessionId) {
20239
+ const filePath = yoloFilePath(sessionId);
20240
+ if (!(0, import_fs2.existsSync)(filePath)) return null;
20241
+ try {
20242
+ const raw = (0, import_fs2.readFileSync)(filePath, "utf-8");
20243
+ const state = JSON.parse(raw);
20244
+ if (state.expiresAt && new Date(state.expiresAt) <= /* @__PURE__ */ new Date()) {
20245
+ try {
20246
+ (0, import_fs2.unlinkSync)(filePath);
20247
+ } catch {
20248
+ }
20249
+ return null;
20250
+ }
20251
+ return state;
20252
+ } catch {
20253
+ return null;
20254
+ }
20255
+ }
20256
+ function activateYolo(sessionId, durationMinutes, bypassDeny = false) {
20257
+ const now = /* @__PURE__ */ new Date();
20258
+ const state = {
20259
+ activatedAt: now.toISOString(),
20260
+ expiresAt: durationMinutes ? new Date(now.getTime() + durationMinutes * 6e4).toISOString() : null,
20261
+ bypassDeny
20262
+ };
20263
+ (0, import_fs2.writeFileSync)(yoloFilePath(sessionId), JSON.stringify(state), "utf-8");
20264
+ return state;
20265
+ }
20266
+ var YOLO_PATTERN = /^echo\s+__WARDEN_YOLO_(ACTIVATE|DEACTIVATE|STATUS)__(?::(\w+))?$/;
20267
+ function parseYoloCommand(command) {
20268
+ const match = command.trim().match(YOLO_PATTERN);
20269
+ if (!match) return null;
20270
+ const action = match[1].toLowerCase();
20271
+ const param = match[2] || null;
20272
+ if (action === "activate") {
20273
+ let durationMinutes = null;
20274
+ if (param && param !== "session") {
20275
+ const m = param.match(/^(\d+)m?$/);
20276
+ if (m) {
20277
+ durationMinutes = parseInt(m[1], 10);
20278
+ } else {
20279
+ return null;
20280
+ }
20281
+ }
20282
+ return { action, durationMinutes };
20283
+ }
20284
+ return { action, durationMinutes: null };
20285
+ }
20286
+ function deactivateYolo(sessionId) {
20287
+ const filePath = yoloFilePath(sessionId);
20288
+ if (!(0, import_fs2.existsSync)(filePath)) return false;
20289
+ try {
20290
+ (0, import_fs2.unlinkSync)(filePath);
20291
+ return true;
20292
+ } catch {
20293
+ return false;
20294
+ }
20295
+ }
20296
+
20143
20297
  // src/index.ts
20144
20298
  var MAX_STDIN_SIZE = 1024 * 1024;
20145
20299
  async function main() {
@@ -20174,7 +20328,54 @@ async function main() {
20174
20328
  if (!command || typeof command !== "string") {
20175
20329
  process.exit(0);
20176
20330
  }
20331
+ const yoloCmd = parseYoloCommand(command);
20332
+ if (yoloCmd) {
20333
+ let msg2;
20334
+ if (yoloCmd.action === "activate") {
20335
+ const state = activateYolo(input.session_id, yoloCmd.durationMinutes);
20336
+ const expiryInfo = state.expiresAt ? `expires at ${new Date(state.expiresAt).toLocaleTimeString()}` : "full session, no expiry";
20337
+ msg2 = `[warden] YOLO mode activated (${expiryInfo}). Always-deny commands are still blocked. Use \`echo __WARDEN_YOLO_DEACTIVATE__\` to turn off.`;
20338
+ } else if (yoloCmd.action === "deactivate") {
20339
+ deactivateYolo(input.session_id);
20340
+ msg2 = "[warden] YOLO mode deactivated. Normal rule evaluation resumed.";
20341
+ } else {
20342
+ const state = getYoloState(input.session_id);
20343
+ if (state) {
20344
+ const expiryInfo = state.expiresAt ? `expires at ${new Date(state.expiresAt).toLocaleTimeString()}` : "full session";
20345
+ msg2 = `[warden] YOLO mode is active (${expiryInfo})`;
20346
+ } else {
20347
+ msg2 = "[warden] YOLO mode is not active";
20348
+ }
20349
+ }
20350
+ const output2 = {
20351
+ hookSpecificOutput: {
20352
+ hookEventName: "PreToolUse",
20353
+ permissionDecision: "allow",
20354
+ permissionDecisionReason: msg2
20355
+ }
20356
+ };
20357
+ process.stdout.write(JSON.stringify(output2));
20358
+ process.exit(0);
20359
+ }
20177
20360
  const config = loadConfig(input.cwd);
20361
+ const yoloState = getYoloState(input.session_id);
20362
+ if (yoloState) {
20363
+ const parsed2 = parseCommand(command);
20364
+ const result2 = evaluate(parsed2, config);
20365
+ if (result2.decision === "deny" && !yoloState.bypassDeny) {
20366
+ } else {
20367
+ const expiryInfo = yoloState.expiresAt ? `expires ${new Date(yoloState.expiresAt).toLocaleTimeString()}` : "full session";
20368
+ const output2 = {
20369
+ hookSpecificOutput: {
20370
+ hookEventName: "PreToolUse",
20371
+ permissionDecision: "allow",
20372
+ permissionDecisionReason: `[warden] YOLO mode active (${expiryInfo})`
20373
+ }
20374
+ };
20375
+ process.stdout.write(JSON.stringify(output2));
20376
+ process.exit(0);
20377
+ }
20378
+ }
20178
20379
  const parsed = parseCommand(command);
20179
20380
  const result = evaluate(parsed, config);
20180
20381
  if (result.decision === "allow") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-warden",
3
- "version": "1.8.2",
3
+ "version": "1.10.0",
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",