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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +23 -0
- package/config/warden.default.yaml +28 -1
- package/dist/index.cjs +210 -9
- 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
|
@@ -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") {
|