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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +23 -0
- package/config/warden.default.yaml +28 -1
- package/dist/index.cjs +148 -2
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-warden",
|
|
3
|
-
"version": "1.
|
|
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
|
|
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") {
|