claude-warden 1.8.3 → 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.3",
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
@@ -19980,6 +19980,35 @@ function tryLoadFile(filePath) {
19980
19980
  }
19981
19981
  return null;
19982
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
+ }
19983
20012
  function extractLayer(raw) {
19984
20013
  const rules = Array.isArray(raw.rules) ? raw.rules : [];
19985
20014
  for (const rule of rules) {
@@ -19998,6 +20027,7 @@ function extractLayer(raw) {
19998
20027
  }
19999
20028
  }
20000
20029
  }
20030
+ warnArgPatternCommandMismatch(rule);
20001
20031
  }
20002
20032
  }
20003
20033
  return {
@@ -20108,12 +20138,15 @@ function formatSystemMessage(decision, rawCommand, details) {
20108
20138
  return ` Option A: Allow all \`${d.command}\` \u2192 \`/claude-warden:warden-allow ${d.command}\`
20109
20139
  Option B: Allow only \`${d.command} ${sub}\` \u2192 \`/claude-warden:warden-allow ${d.command} ${sub}\``;
20110
20140
  });
20141
+ const yoloHint = "Tip: `/claude-warden:yolo` to temporarily allow all commands";
20111
20142
  if (subcommandHints.length > 0) {
20112
20143
  return `${header}
20113
20144
  ${subcommandHints.join("\n")}
20114
- See /claude-warden:warden-allow`;
20145
+ See /claude-warden:warden-allow
20146
+ ${yoloHint}`;
20115
20147
  }
20116
- 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}`;
20117
20150
  }
20118
20151
  const lines = ["[warden] Command blocked", ""];
20119
20152
  if (relevant.length > 0) {
@@ -20195,6 +20228,72 @@ function sendNotification(title, message, config) {
20195
20228
  }
20196
20229
  }
20197
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
+
20198
20297
  // src/index.ts
20199
20298
  var MAX_STDIN_SIZE = 1024 * 1024;
20200
20299
  async function main() {
@@ -20229,7 +20328,54 @@ async function main() {
20229
20328
  if (!command || typeof command !== "string") {
20230
20329
  process.exit(0);
20231
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
+ }
20232
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
+ }
20233
20379
  const parsed = parseCommand(command);
20234
20380
  const result = evaluate(parsed, config);
20235
20381
  if (result.decision === "allow") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-warden",
3
- "version": "1.8.3",
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",