claude-warden 1.7.0 → 1.8.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.
- package/.claude-plugin/plugin.json +1 -1
- package/dist/index.cjs +329 -27
- package/package.json +6 -4
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-warden",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.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/index.cjs
CHANGED
|
@@ -18379,12 +18379,92 @@ function parseCommand(input) {
|
|
|
18379
18379
|
return { commands: [], hasSubshell: true, subshellCommands: [], parseError: true };
|
|
18380
18380
|
}
|
|
18381
18381
|
}
|
|
18382
|
+
const fallback = regexFallbackParse(input);
|
|
18383
|
+
if (fallback) {
|
|
18384
|
+
return { commands: [fallback], hasSubshell: false, subshellCommands: [], parseError: false };
|
|
18385
|
+
}
|
|
18382
18386
|
return { commands: [], hasSubshell: false, subshellCommands: [], parseError: true };
|
|
18383
18387
|
}
|
|
18384
18388
|
}
|
|
18389
|
+
function regexFallbackParse(input) {
|
|
18390
|
+
const trimmed = input.trim();
|
|
18391
|
+
if (!trimmed) return null;
|
|
18392
|
+
let inSingle = false;
|
|
18393
|
+
let inDouble = false;
|
|
18394
|
+
for (let i = 0; i < trimmed.length; i++) {
|
|
18395
|
+
const ch = trimmed[i];
|
|
18396
|
+
if (ch === "\\" && inDouble) {
|
|
18397
|
+
i++;
|
|
18398
|
+
continue;
|
|
18399
|
+
}
|
|
18400
|
+
if (ch === "'" && !inDouble) {
|
|
18401
|
+
inSingle = !inSingle;
|
|
18402
|
+
continue;
|
|
18403
|
+
}
|
|
18404
|
+
if (ch === '"' && !inSingle) {
|
|
18405
|
+
inDouble = !inDouble;
|
|
18406
|
+
continue;
|
|
18407
|
+
}
|
|
18408
|
+
if (!inSingle && !inDouble && (ch === "|" || ch === ";" || ch === "&" && trimmed[i + 1] === "&")) {
|
|
18409
|
+
return null;
|
|
18410
|
+
}
|
|
18411
|
+
}
|
|
18412
|
+
const envPrefixes = [];
|
|
18413
|
+
let rest = trimmed;
|
|
18414
|
+
while (/^[A-Za-z_][A-Za-z0-9_]*=/.test(rest)) {
|
|
18415
|
+
const match = rest.match(/^([A-Za-z_][A-Za-z0-9_]*=\S*)\s*/);
|
|
18416
|
+
if (!match) break;
|
|
18417
|
+
envPrefixes.push(match[1]);
|
|
18418
|
+
rest = rest.slice(match[0].length);
|
|
18419
|
+
}
|
|
18420
|
+
if (!rest) return null;
|
|
18421
|
+
const cmdMatch = rest.match(/^(\S+)/);
|
|
18422
|
+
if (!cmdMatch) return null;
|
|
18423
|
+
const originalCommand = cmdMatch[1];
|
|
18424
|
+
const command = originalCommand.includes("/") ? (0, import_path.basename)(originalCommand) : originalCommand;
|
|
18425
|
+
const argsStr = rest.slice(cmdMatch[0].length).trim();
|
|
18426
|
+
const args2 = [];
|
|
18427
|
+
let current = "";
|
|
18428
|
+
let qSingle = false;
|
|
18429
|
+
let qDouble = false;
|
|
18430
|
+
for (let i = 0; i < argsStr.length; i++) {
|
|
18431
|
+
const ch = argsStr[i];
|
|
18432
|
+
if (ch === "\\" && qDouble && i + 1 < argsStr.length) {
|
|
18433
|
+
current += argsStr[++i];
|
|
18434
|
+
continue;
|
|
18435
|
+
}
|
|
18436
|
+
if (ch === "'" && !qDouble) {
|
|
18437
|
+
qSingle = !qSingle;
|
|
18438
|
+
continue;
|
|
18439
|
+
}
|
|
18440
|
+
if (ch === '"' && !qSingle) {
|
|
18441
|
+
qDouble = !qDouble;
|
|
18442
|
+
continue;
|
|
18443
|
+
}
|
|
18444
|
+
if (!qSingle && !qDouble && /\s/.test(ch)) {
|
|
18445
|
+
if (current) {
|
|
18446
|
+
args2.push(current);
|
|
18447
|
+
current = "";
|
|
18448
|
+
}
|
|
18449
|
+
continue;
|
|
18450
|
+
}
|
|
18451
|
+
current += ch;
|
|
18452
|
+
}
|
|
18453
|
+
if (current) args2.push(current);
|
|
18454
|
+
return { command, originalCommand, args: args2, envPrefixes, raw: trimmed };
|
|
18455
|
+
}
|
|
18385
18456
|
|
|
18386
18457
|
// src/evaluator.ts
|
|
18387
18458
|
var import_os = require("os");
|
|
18459
|
+
function safeRegexTest(pattern, input) {
|
|
18460
|
+
try {
|
|
18461
|
+
return new RegExp(pattern).test(input);
|
|
18462
|
+
} catch {
|
|
18463
|
+
process.stderr.write(`[warden] Warning: invalid regex pattern: ${pattern}
|
|
18464
|
+
`);
|
|
18465
|
+
return false;
|
|
18466
|
+
}
|
|
18467
|
+
}
|
|
18388
18468
|
function commandMatchesName(cmd, name) {
|
|
18389
18469
|
if (name.startsWith("/")) {
|
|
18390
18470
|
return cmd.originalCommand === name;
|
|
@@ -18394,7 +18474,11 @@ function commandMatchesName(cmd, name) {
|
|
|
18394
18474
|
}
|
|
18395
18475
|
return cmd.command === name;
|
|
18396
18476
|
}
|
|
18397
|
-
|
|
18477
|
+
var MAX_RECURSION_DEPTH = 10;
|
|
18478
|
+
function evaluate(parsed, config, depth = 0) {
|
|
18479
|
+
if (depth > MAX_RECURSION_DEPTH) {
|
|
18480
|
+
return { decision: "ask", reason: "Maximum recursion depth exceeded", details: [] };
|
|
18481
|
+
}
|
|
18398
18482
|
if (parsed.parseError) {
|
|
18399
18483
|
return { decision: "ask", reason: "Could not parse command safely", details: [] };
|
|
18400
18484
|
}
|
|
@@ -18404,7 +18488,7 @@ function evaluate(parsed, config) {
|
|
|
18404
18488
|
if (parsed.hasSubshell && parsed.subshellCommands.length > 0) {
|
|
18405
18489
|
for (const subCmd of parsed.subshellCommands) {
|
|
18406
18490
|
const subParsed = parseCommand(subCmd);
|
|
18407
|
-
const subResult = evaluate(subParsed, config);
|
|
18491
|
+
const subResult = evaluate(subParsed, config, depth + 1);
|
|
18408
18492
|
if (subResult.decision === "deny") {
|
|
18409
18493
|
return { decision: "deny", reason: `Subshell command: ${subResult.reason}`, details: subResult.details };
|
|
18410
18494
|
}
|
|
@@ -18417,7 +18501,7 @@ function evaluate(parsed, config) {
|
|
|
18417
18501
|
}
|
|
18418
18502
|
const details = [];
|
|
18419
18503
|
for (const cmd of parsed.commands) {
|
|
18420
|
-
details.push(evaluateCommand(cmd, config));
|
|
18504
|
+
details.push(evaluateCommand(cmd, config, depth));
|
|
18421
18505
|
}
|
|
18422
18506
|
const decisions = details.map((d) => d.decision);
|
|
18423
18507
|
if (decisions.includes("deny")) {
|
|
@@ -18438,7 +18522,7 @@ function evaluate(parsed, config) {
|
|
|
18438
18522
|
}
|
|
18439
18523
|
return { decision: "allow", reason: "All commands are safe", details };
|
|
18440
18524
|
}
|
|
18441
|
-
function evaluateCommand(cmd, config) {
|
|
18525
|
+
function evaluateCommand(cmd, config, depth = 0) {
|
|
18442
18526
|
const { command, args: args2 } = cmd;
|
|
18443
18527
|
for (const layer of config.layers) {
|
|
18444
18528
|
if (layer.alwaysDeny.some((name) => commandMatchesName(cmd, name))) {
|
|
@@ -18449,28 +18533,52 @@ function evaluateCommand(cmd, config) {
|
|
|
18449
18533
|
}
|
|
18450
18534
|
}
|
|
18451
18535
|
if ((command === "ssh" || command === "scp" || command === "rsync") && config.trustedSSHHosts?.length) {
|
|
18452
|
-
const sshResult = evaluateSSHCommand(cmd, config);
|
|
18536
|
+
const sshResult = evaluateSSHCommand(cmd, config, depth);
|
|
18453
18537
|
if (sshResult) return sshResult;
|
|
18454
18538
|
}
|
|
18455
18539
|
if (command === "docker" && config.trustedDockerContainers?.length) {
|
|
18456
|
-
const dockerResult = evaluateDockerExec(cmd, config);
|
|
18540
|
+
const dockerResult = evaluateDockerExec(cmd, config, depth);
|
|
18457
18541
|
if (dockerResult) return dockerResult;
|
|
18458
18542
|
}
|
|
18459
18543
|
if (command === "kubectl" && config.trustedKubectlContexts?.length) {
|
|
18460
|
-
const kubectlResult = evaluateKubectlExec(cmd, config);
|
|
18544
|
+
const kubectlResult = evaluateKubectlExec(cmd, config, depth);
|
|
18461
18545
|
if (kubectlResult) return kubectlResult;
|
|
18462
18546
|
}
|
|
18463
18547
|
if (command === "sprite" && config.trustedSprites?.length) {
|
|
18464
|
-
const spriteResult = evaluateSpriteExec(cmd, config);
|
|
18548
|
+
const spriteResult = evaluateSpriteExec(cmd, config, depth);
|
|
18465
18549
|
if (spriteResult) return spriteResult;
|
|
18466
18550
|
}
|
|
18551
|
+
if (command === "xargs") {
|
|
18552
|
+
return evaluateXargsCommand(cmd, config, depth);
|
|
18553
|
+
}
|
|
18554
|
+
const mergedRule = collectMergedRule(cmd, config);
|
|
18555
|
+
if (mergedRule) {
|
|
18556
|
+
return evaluateRule(cmd, mergedRule);
|
|
18557
|
+
}
|
|
18558
|
+
return { command, args: args2, decision: config.defaultDecision, reason: `No rule for "${command}"`, matchedRule: "default" };
|
|
18559
|
+
}
|
|
18560
|
+
function collectMergedRule(cmd, config) {
|
|
18561
|
+
const matchingRules = [];
|
|
18467
18562
|
for (const layer of config.layers) {
|
|
18468
18563
|
const rule = layer.rules.find((r) => commandMatchesName(cmd, r.command));
|
|
18469
18564
|
if (rule) {
|
|
18470
|
-
|
|
18565
|
+
matchingRules.push(rule);
|
|
18566
|
+
if (rule.override) break;
|
|
18471
18567
|
}
|
|
18472
18568
|
}
|
|
18473
|
-
|
|
18569
|
+
if (matchingRules.length === 0) return null;
|
|
18570
|
+
if (matchingRules.length === 1) return matchingRules[0];
|
|
18571
|
+
const mergedPatterns = [];
|
|
18572
|
+
for (const rule of matchingRules) {
|
|
18573
|
+
if (rule.argPatterns) {
|
|
18574
|
+
mergedPatterns.push(...rule.argPatterns);
|
|
18575
|
+
}
|
|
18576
|
+
}
|
|
18577
|
+
return {
|
|
18578
|
+
command: matchingRules[0].command,
|
|
18579
|
+
default: matchingRules[0].default,
|
|
18580
|
+
argPatterns: mergedPatterns
|
|
18581
|
+
};
|
|
18474
18582
|
}
|
|
18475
18583
|
function evaluateRule(cmd, rule) {
|
|
18476
18584
|
const { command, args: args2 } = cmd;
|
|
@@ -18482,10 +18590,10 @@ function evaluateRule(cmd, rule) {
|
|
|
18482
18590
|
matched = matched && m.noArgs === (args2.length === 0);
|
|
18483
18591
|
}
|
|
18484
18592
|
if (m.argsMatch && matched) {
|
|
18485
|
-
matched = m.argsMatch.some((re) =>
|
|
18593
|
+
matched = m.argsMatch.some((re) => safeRegexTest(re, argsJoined));
|
|
18486
18594
|
}
|
|
18487
18595
|
if (m.anyArgMatches && matched) {
|
|
18488
|
-
matched = args2.some((arg) => m.anyArgMatches.some((re) =>
|
|
18596
|
+
matched = args2.some((arg) => m.anyArgMatches.some((re) => safeRegexTest(re, arg)));
|
|
18489
18597
|
}
|
|
18490
18598
|
if (m.argCount && matched) {
|
|
18491
18599
|
if (m.argCount.min !== void 0) matched = matched && args2.length >= m.argCount.min;
|
|
@@ -18510,6 +18618,126 @@ function evaluateRule(cmd, rule) {
|
|
|
18510
18618
|
matchedRule: `${command}:default`
|
|
18511
18619
|
};
|
|
18512
18620
|
}
|
|
18621
|
+
var XARGS_SHORT_FLAGS_WITH_VALUE = /* @__PURE__ */ new Set(["E", "I", "L", "n", "P", "s", "S", "d", "a"]);
|
|
18622
|
+
var XARGS_SHORT_FLAGS_NO_VALUE = /* @__PURE__ */ new Set(["0", "e", "o", "p", "r", "t", "x"]);
|
|
18623
|
+
var XARGS_LONG_FLAGS_WITH_VALUE = /* @__PURE__ */ new Set([
|
|
18624
|
+
"--eof",
|
|
18625
|
+
"--replace",
|
|
18626
|
+
"--max-lines",
|
|
18627
|
+
"--max-args",
|
|
18628
|
+
"--max-procs",
|
|
18629
|
+
"--max-chars",
|
|
18630
|
+
"--arg-file",
|
|
18631
|
+
"--delimiter"
|
|
18632
|
+
]);
|
|
18633
|
+
var XARGS_LONG_FLAGS_NO_VALUE = /* @__PURE__ */ new Set([
|
|
18634
|
+
"--null",
|
|
18635
|
+
"--exit",
|
|
18636
|
+
"--open-tty",
|
|
18637
|
+
"--interactive",
|
|
18638
|
+
"--no-run-if-empty",
|
|
18639
|
+
"--verbose",
|
|
18640
|
+
"--show-limits"
|
|
18641
|
+
]);
|
|
18642
|
+
function parseXargsSubcommand(args2) {
|
|
18643
|
+
let i = 0;
|
|
18644
|
+
while (i < args2.length) {
|
|
18645
|
+
const arg = args2[i];
|
|
18646
|
+
if (arg === "--") {
|
|
18647
|
+
i++;
|
|
18648
|
+
break;
|
|
18649
|
+
}
|
|
18650
|
+
if (!arg.startsWith("-") || arg === "-") {
|
|
18651
|
+
break;
|
|
18652
|
+
}
|
|
18653
|
+
if (arg.startsWith("--")) {
|
|
18654
|
+
const eqIndex = arg.indexOf("=");
|
|
18655
|
+
const longFlag = eqIndex === -1 ? arg : arg.slice(0, eqIndex);
|
|
18656
|
+
if (XARGS_LONG_FLAGS_WITH_VALUE.has(longFlag)) {
|
|
18657
|
+
if (eqIndex !== -1) {
|
|
18658
|
+
i++;
|
|
18659
|
+
continue;
|
|
18660
|
+
}
|
|
18661
|
+
if (i + 1 >= args2.length) return { subcommand: null, unresolved: true };
|
|
18662
|
+
i += 2;
|
|
18663
|
+
continue;
|
|
18664
|
+
}
|
|
18665
|
+
if (XARGS_LONG_FLAGS_NO_VALUE.has(longFlag)) {
|
|
18666
|
+
i++;
|
|
18667
|
+
continue;
|
|
18668
|
+
}
|
|
18669
|
+
return { subcommand: null, unresolved: true };
|
|
18670
|
+
}
|
|
18671
|
+
const short = arg[1];
|
|
18672
|
+
if (XARGS_SHORT_FLAGS_WITH_VALUE.has(short)) {
|
|
18673
|
+
if (arg.length > 2) {
|
|
18674
|
+
i++;
|
|
18675
|
+
continue;
|
|
18676
|
+
}
|
|
18677
|
+
if (i + 1 >= args2.length) return { subcommand: null, unresolved: true };
|
|
18678
|
+
i += 2;
|
|
18679
|
+
continue;
|
|
18680
|
+
}
|
|
18681
|
+
const grouped = arg.slice(1).split("");
|
|
18682
|
+
const allKnownNoValue = grouped.every((ch) => XARGS_SHORT_FLAGS_NO_VALUE.has(ch));
|
|
18683
|
+
if (allKnownNoValue) {
|
|
18684
|
+
i++;
|
|
18685
|
+
continue;
|
|
18686
|
+
}
|
|
18687
|
+
return { subcommand: null, unresolved: true };
|
|
18688
|
+
}
|
|
18689
|
+
if (i >= args2.length) {
|
|
18690
|
+
return {
|
|
18691
|
+
unresolved: false,
|
|
18692
|
+
subcommand: {
|
|
18693
|
+
command: "echo",
|
|
18694
|
+
originalCommand: "echo",
|
|
18695
|
+
args: [],
|
|
18696
|
+
envPrefixes: [],
|
|
18697
|
+
raw: "echo"
|
|
18698
|
+
}
|
|
18699
|
+
};
|
|
18700
|
+
}
|
|
18701
|
+
const subcommand = args2[i];
|
|
18702
|
+
const subArgs = args2.slice(i + 1);
|
|
18703
|
+
return {
|
|
18704
|
+
unresolved: false,
|
|
18705
|
+
subcommand: {
|
|
18706
|
+
command: subcommand,
|
|
18707
|
+
originalCommand: subcommand,
|
|
18708
|
+
args: subArgs,
|
|
18709
|
+
envPrefixes: [],
|
|
18710
|
+
raw: [subcommand, ...subArgs].join(" ")
|
|
18711
|
+
}
|
|
18712
|
+
};
|
|
18713
|
+
}
|
|
18714
|
+
function evaluateXargsCommand(cmd, config, depth = 0) {
|
|
18715
|
+
const { command, args: args2 } = cmd;
|
|
18716
|
+
const { subcommand, unresolved } = parseXargsSubcommand(args2);
|
|
18717
|
+
if (unresolved || !subcommand) {
|
|
18718
|
+
return {
|
|
18719
|
+
command,
|
|
18720
|
+
args: args2,
|
|
18721
|
+
decision: "ask",
|
|
18722
|
+
reason: "xargs subcommand could not be resolved safely",
|
|
18723
|
+
matchedRule: "xargs:subcommand"
|
|
18724
|
+
};
|
|
18725
|
+
}
|
|
18726
|
+
const parsed = {
|
|
18727
|
+
commands: [subcommand],
|
|
18728
|
+
hasSubshell: false,
|
|
18729
|
+
subshellCommands: [],
|
|
18730
|
+
parseError: false
|
|
18731
|
+
};
|
|
18732
|
+
const result = evaluate(parsed, config, depth + 1);
|
|
18733
|
+
return {
|
|
18734
|
+
command,
|
|
18735
|
+
args: args2,
|
|
18736
|
+
decision: result.decision,
|
|
18737
|
+
reason: `xargs subcommand "${subcommand.command}": ${result.reason}`,
|
|
18738
|
+
matchedRule: "xargs:subcommand"
|
|
18739
|
+
};
|
|
18740
|
+
}
|
|
18513
18741
|
var SSH_FLAGS_WITH_VALUE = /* @__PURE__ */ new Set([
|
|
18514
18742
|
"-b",
|
|
18515
18743
|
"-c",
|
|
@@ -18604,7 +18832,7 @@ function parseSSHArgs(args2) {
|
|
|
18604
18832
|
}
|
|
18605
18833
|
return {
|
|
18606
18834
|
host,
|
|
18607
|
-
remoteCommand: remoteArgs.length > 0 ? remoteArgs.join(" ") : null
|
|
18835
|
+
remoteCommand: remoteArgs.length > 0 ? remoteArgs.map(shellQuote).join(" ") : null
|
|
18608
18836
|
};
|
|
18609
18837
|
}
|
|
18610
18838
|
function extractHostFromRemotePath(args2) {
|
|
@@ -18614,7 +18842,7 @@ function extractHostFromRemotePath(args2) {
|
|
|
18614
18842
|
}
|
|
18615
18843
|
return null;
|
|
18616
18844
|
}
|
|
18617
|
-
function evaluateSSHCommand(cmd, config) {
|
|
18845
|
+
function evaluateSSHCommand(cmd, config, depth = 0) {
|
|
18618
18846
|
const { command, args: args2 } = cmd;
|
|
18619
18847
|
const trustedHosts = config.trustedSSHHosts || [];
|
|
18620
18848
|
if (command === "scp" || command === "rsync") {
|
|
@@ -18622,6 +18850,24 @@ function evaluateSSHCommand(cmd, config) {
|
|
|
18622
18850
|
if (!host2) return null;
|
|
18623
18851
|
const target2 = findMatchingTarget(host2, trustedHosts);
|
|
18624
18852
|
if (!target2) return null;
|
|
18853
|
+
if (target2.allowAll || !target2.overrides) {
|
|
18854
|
+
return {
|
|
18855
|
+
command,
|
|
18856
|
+
args: args2,
|
|
18857
|
+
decision: "allow",
|
|
18858
|
+
reason: `Trusted SSH host "${host2}"${target2.allowAll ? " (allowAll)" : ""}`,
|
|
18859
|
+
matchedRule: "trustedSSHHosts"
|
|
18860
|
+
};
|
|
18861
|
+
}
|
|
18862
|
+
if (target2.overrides.alwaysDeny.some((name) => name === command)) {
|
|
18863
|
+
return {
|
|
18864
|
+
command,
|
|
18865
|
+
args: args2,
|
|
18866
|
+
decision: "deny",
|
|
18867
|
+
reason: `Trusted SSH host "${host2}": "${command}" blocked by overrides`,
|
|
18868
|
+
matchedRule: "trustedSSHHosts"
|
|
18869
|
+
};
|
|
18870
|
+
}
|
|
18625
18871
|
return {
|
|
18626
18872
|
command,
|
|
18627
18873
|
args: args2,
|
|
@@ -18653,7 +18899,7 @@ function evaluateSSHCommand(cmd, config) {
|
|
|
18653
18899
|
};
|
|
18654
18900
|
}
|
|
18655
18901
|
const parsed = parseCommand(remoteCommand);
|
|
18656
|
-
const result = evaluate(parsed, configWithContextOverrides(config, target));
|
|
18902
|
+
const result = evaluate(parsed, configWithContextOverrides(config, target), depth + 1);
|
|
18657
18903
|
return {
|
|
18658
18904
|
command,
|
|
18659
18905
|
args: args2,
|
|
@@ -18673,6 +18919,12 @@ var DOCKER_EXEC_FLAGS_WITH_VALUE = /* @__PURE__ */ new Set([
|
|
|
18673
18919
|
"--detach-keys"
|
|
18674
18920
|
]);
|
|
18675
18921
|
var INTERACTIVE_SHELLS = /* @__PURE__ */ new Set(["bash", "sh", "zsh"]);
|
|
18922
|
+
function shellQuote(arg) {
|
|
18923
|
+
if (/[\s"'\\$`!#&|;()<>]/.test(arg)) {
|
|
18924
|
+
return `'${arg.replace(/'/g, "'\\''")}'`;
|
|
18925
|
+
}
|
|
18926
|
+
return arg;
|
|
18927
|
+
}
|
|
18676
18928
|
function configWithContextOverrides(config, target) {
|
|
18677
18929
|
const overrideLayers = [];
|
|
18678
18930
|
if (target?.overrides) overrideLayers.push(target.overrides);
|
|
@@ -18683,7 +18935,7 @@ function configWithContextOverrides(config, target) {
|
|
|
18683
18935
|
layers: [...overrideLayers, ...config.layers]
|
|
18684
18936
|
};
|
|
18685
18937
|
}
|
|
18686
|
-
function evaluateRemoteCommand(remoteArgs, config, target) {
|
|
18938
|
+
function evaluateRemoteCommand(remoteArgs, config, target, depth = 0) {
|
|
18687
18939
|
if (target?.allowAll) {
|
|
18688
18940
|
return { decision: "allow", reason: "allowAll target", details: [] };
|
|
18689
18941
|
}
|
|
@@ -18698,7 +18950,7 @@ function evaluateRemoteCommand(remoteArgs, config, target) {
|
|
|
18698
18950
|
if (INTERACTIVE_SHELLS.has(remoteCmd) && remoteArgs[1] === "-c" && remoteArgs.length >= 3) {
|
|
18699
18951
|
const innerCommand = remoteArgs.slice(2).join(" ");
|
|
18700
18952
|
const parsed2 = parseCommand(innerCommand);
|
|
18701
|
-
return evaluate(parsed2, overriddenConfig);
|
|
18953
|
+
return evaluate(parsed2, overriddenConfig, depth + 1);
|
|
18702
18954
|
}
|
|
18703
18955
|
const parsed = {
|
|
18704
18956
|
commands: [{ command: remoteCmd, originalCommand: remoteCmd, args: remoteArgs.slice(1), envPrefixes: [], raw: remoteArgs.join(" ") }],
|
|
@@ -18706,7 +18958,7 @@ function evaluateRemoteCommand(remoteArgs, config, target) {
|
|
|
18706
18958
|
subshellCommands: [],
|
|
18707
18959
|
parseError: false
|
|
18708
18960
|
};
|
|
18709
|
-
return evaluate(parsed, overriddenConfig);
|
|
18961
|
+
return evaluate(parsed, overriddenConfig, depth + 1);
|
|
18710
18962
|
}
|
|
18711
18963
|
function parseDockerExecArgs(args2) {
|
|
18712
18964
|
let target = null;
|
|
@@ -18735,14 +18987,14 @@ function parseDockerExecArgs(args2) {
|
|
|
18735
18987
|
}
|
|
18736
18988
|
return { target, remoteArgs };
|
|
18737
18989
|
}
|
|
18738
|
-
function evaluateDockerExec(cmd, config) {
|
|
18990
|
+
function evaluateDockerExec(cmd, config, depth = 0) {
|
|
18739
18991
|
const { command, args: args2 } = cmd;
|
|
18740
18992
|
if (args2[0] !== "exec") return null;
|
|
18741
18993
|
const { target: containerName, remoteArgs } = parseDockerExecArgs(args2.slice(1));
|
|
18742
18994
|
if (!containerName) return null;
|
|
18743
18995
|
const matched = findMatchingTarget(containerName, config.trustedDockerContainers || []);
|
|
18744
18996
|
if (!matched) return null;
|
|
18745
|
-
const result = evaluateRemoteCommand(remoteArgs, config, matched);
|
|
18997
|
+
const result = evaluateRemoteCommand(remoteArgs, config, matched, depth);
|
|
18746
18998
|
return {
|
|
18747
18999
|
command,
|
|
18748
19000
|
args: args2,
|
|
@@ -18815,14 +19067,14 @@ function parseKubectlExecArgs(args2) {
|
|
|
18815
19067
|
}
|
|
18816
19068
|
return { context, pod, remoteArgs };
|
|
18817
19069
|
}
|
|
18818
|
-
function evaluateKubectlExec(cmd, config) {
|
|
19070
|
+
function evaluateKubectlExec(cmd, config, depth = 0) {
|
|
18819
19071
|
const { command, args: args2 } = cmd;
|
|
18820
19072
|
if (args2[0] !== "exec") return null;
|
|
18821
19073
|
const { context, pod, remoteArgs } = parseKubectlExecArgs(args2.slice(1));
|
|
18822
19074
|
if (!context) return null;
|
|
18823
19075
|
const matched = findMatchingTarget(context, config.trustedKubectlContexts || []);
|
|
18824
19076
|
if (!matched) return null;
|
|
18825
|
-
const result = evaluateRemoteCommand(remoteArgs, config, matched);
|
|
19077
|
+
const result = evaluateRemoteCommand(remoteArgs, config, matched, depth);
|
|
18826
19078
|
return {
|
|
18827
19079
|
command,
|
|
18828
19080
|
args: args2,
|
|
@@ -18882,13 +19134,13 @@ function parseSpriteExecArgs(args2) {
|
|
|
18882
19134
|
}
|
|
18883
19135
|
return { spriteName, remoteArgs };
|
|
18884
19136
|
}
|
|
18885
|
-
function evaluateSpriteExec(cmd, config) {
|
|
19137
|
+
function evaluateSpriteExec(cmd, config, depth = 0) {
|
|
18886
19138
|
const { command, args: args2 } = cmd;
|
|
18887
19139
|
const { spriteName, remoteArgs } = parseSpriteExecArgs(args2);
|
|
18888
19140
|
if (!spriteName) return null;
|
|
18889
19141
|
const matched = findMatchingTarget(spriteName, config.trustedSprites || []);
|
|
18890
19142
|
if (!matched) return null;
|
|
18891
|
-
const result = evaluateRemoteCommand(remoteArgs, config, matched);
|
|
19143
|
+
const result = evaluateRemoteCommand(remoteArgs, config, matched, depth);
|
|
18892
19144
|
return {
|
|
18893
19145
|
command,
|
|
18894
19146
|
args: args2,
|
|
@@ -19231,6 +19483,7 @@ var DEFAULT_CONFIG = {
|
|
|
19231
19483
|
"xcode-select",
|
|
19232
19484
|
"xcrun",
|
|
19233
19485
|
"xcodebuild",
|
|
19486
|
+
"networkQuality",
|
|
19234
19487
|
// Misc safe
|
|
19235
19488
|
"cd",
|
|
19236
19489
|
"pushd",
|
|
@@ -19339,6 +19592,7 @@ var DEFAULT_CONFIG = {
|
|
|
19339
19592
|
// npx / bunx — package runners
|
|
19340
19593
|
pkgRunnerRule("npx"),
|
|
19341
19594
|
pkgRunnerRule("bunx"),
|
|
19595
|
+
pkgRunnerRule("pnpx"),
|
|
19342
19596
|
// npm / pnpm / yarn — package managers
|
|
19343
19597
|
pkgManagerRule("npm", ["ci", "search", "explain", "prefix", "root", "fund", "doctor", "diff", "pkg", "query", "shrinkwrap"]),
|
|
19344
19598
|
pkgManagerRule("pnpm", ["store", "fetch", "doctor", "patch"]),
|
|
@@ -19592,6 +19846,12 @@ var DEFAULT_CONFIG = {
|
|
|
19592
19846
|
{ command: "codesign", default: "ask", argPatterns: [
|
|
19593
19847
|
{ match: { anyArgMatches: ["^(-vv|--verify|--display|-d)$"] }, decision: "allow", description: "Verify codesign" }
|
|
19594
19848
|
] },
|
|
19849
|
+
{ command: "networksetup", default: "ask", argPatterns: [
|
|
19850
|
+
{ match: { anyArgMatches: ["^-(get|list|show)"] }, decision: "allow", description: "Read-only network configuration queries" }
|
|
19851
|
+
] },
|
|
19852
|
+
{ command: "scutil", default: "ask", argPatterns: [
|
|
19853
|
+
{ match: { anyArgMatches: ["^--get$", "^--dns$", "^--proxy$", "^--nwi$"] }, decision: "allow", description: "Read-only system configuration queries" }
|
|
19854
|
+
] },
|
|
19595
19855
|
{ command: "osascript", default: "ask" },
|
|
19596
19856
|
{ command: "say", default: "ask" },
|
|
19597
19857
|
// --- Process management ---
|
|
@@ -19604,6 +19864,10 @@ var DEFAULT_CONFIG = {
|
|
|
19604
19864
|
};
|
|
19605
19865
|
|
|
19606
19866
|
// src/rules.ts
|
|
19867
|
+
var VALID_DECISIONS = /* @__PURE__ */ new Set(["allow", "deny", "ask"]);
|
|
19868
|
+
function isValidDecision(value) {
|
|
19869
|
+
return VALID_DECISIONS.has(value);
|
|
19870
|
+
}
|
|
19607
19871
|
var USER_CONFIG_PATHS = [
|
|
19608
19872
|
(0, import_path2.join)((0, import_os2.homedir)(), ".claude", "warden.yaml"),
|
|
19609
19873
|
(0, import_path2.join)((0, import_os2.homedir)(), ".claude", "warden.json")
|
|
@@ -19654,15 +19918,36 @@ function tryLoadFile(filePath) {
|
|
|
19654
19918
|
if (parsed && typeof parsed === "object") {
|
|
19655
19919
|
return parsed;
|
|
19656
19920
|
}
|
|
19657
|
-
} catch {
|
|
19921
|
+
} catch (err) {
|
|
19922
|
+
process.stderr.write(`[warden] Warning: failed to parse config ${filePath}: ${err instanceof Error ? err.message : String(err)}
|
|
19923
|
+
`);
|
|
19658
19924
|
}
|
|
19659
19925
|
return null;
|
|
19660
19926
|
}
|
|
19661
19927
|
function extractLayer(raw) {
|
|
19928
|
+
const rules = Array.isArray(raw.rules) ? raw.rules : [];
|
|
19929
|
+
for (const rule of rules) {
|
|
19930
|
+
if (rule && typeof rule === "object") {
|
|
19931
|
+
if (rule.default && !isValidDecision(rule.default)) {
|
|
19932
|
+
process.stderr.write(`[warden] Warning: invalid rule default "${rule.default}" for "${rule.command}", using "ask"
|
|
19933
|
+
`);
|
|
19934
|
+
rule.default = "ask";
|
|
19935
|
+
}
|
|
19936
|
+
if (Array.isArray(rule.argPatterns)) {
|
|
19937
|
+
for (const pattern of rule.argPatterns) {
|
|
19938
|
+
if (pattern?.decision && !isValidDecision(pattern.decision)) {
|
|
19939
|
+
process.stderr.write(`[warden] Warning: invalid pattern decision "${pattern.decision}" for "${rule.command}", using "ask"
|
|
19940
|
+
`);
|
|
19941
|
+
pattern.decision = "ask";
|
|
19942
|
+
}
|
|
19943
|
+
}
|
|
19944
|
+
}
|
|
19945
|
+
}
|
|
19946
|
+
}
|
|
19662
19947
|
return {
|
|
19663
19948
|
alwaysAllow: Array.isArray(raw.alwaysAllow) ? raw.alwaysAllow : [],
|
|
19664
19949
|
alwaysDeny: Array.isArray(raw.alwaysDeny) ? raw.alwaysDeny : [],
|
|
19665
|
-
rules
|
|
19950
|
+
rules
|
|
19666
19951
|
};
|
|
19667
19952
|
}
|
|
19668
19953
|
function parseTrustedList(raw) {
|
|
@@ -19694,7 +19979,12 @@ function mergeNonLayerFields(config, raw) {
|
|
|
19694
19979
|
config.trustedSprites = [...config.trustedSprites || [], ...parseTrustedList(raw.trustedSprites)];
|
|
19695
19980
|
}
|
|
19696
19981
|
if (typeof raw.defaultDecision === "string") {
|
|
19697
|
-
|
|
19982
|
+
if (isValidDecision(raw.defaultDecision)) {
|
|
19983
|
+
config.defaultDecision = raw.defaultDecision;
|
|
19984
|
+
} else {
|
|
19985
|
+
process.stderr.write(`[warden] Warning: invalid defaultDecision "${raw.defaultDecision}", ignoring
|
|
19986
|
+
`);
|
|
19987
|
+
}
|
|
19698
19988
|
}
|
|
19699
19989
|
if (typeof raw.askOnSubshell === "boolean") {
|
|
19700
19990
|
config.askOnSubshell = raw.askOnSubshell;
|
|
@@ -19850,10 +20140,22 @@ function sendNotification(title, message, config) {
|
|
|
19850
20140
|
}
|
|
19851
20141
|
|
|
19852
20142
|
// src/index.ts
|
|
20143
|
+
var MAX_STDIN_SIZE = 1024 * 1024;
|
|
19853
20144
|
async function main() {
|
|
19854
20145
|
let raw = "";
|
|
19855
20146
|
for await (const chunk of process.stdin) {
|
|
19856
20147
|
raw += chunk;
|
|
20148
|
+
if (raw.length > MAX_STDIN_SIZE) {
|
|
20149
|
+
const output2 = {
|
|
20150
|
+
hookSpecificOutput: {
|
|
20151
|
+
hookEventName: "PreToolUse",
|
|
20152
|
+
permissionDecision: "ask",
|
|
20153
|
+
permissionDecisionReason: "[warden] Input exceeds size limit"
|
|
20154
|
+
}
|
|
20155
|
+
};
|
|
20156
|
+
process.stdout.write(JSON.stringify(output2));
|
|
20157
|
+
process.exit(0);
|
|
20158
|
+
}
|
|
19857
20159
|
}
|
|
19858
20160
|
let input;
|
|
19859
20161
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-warden",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.8.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",
|
|
@@ -28,13 +28,15 @@
|
|
|
28
28
|
"README.md",
|
|
29
29
|
"LICENSE"
|
|
30
30
|
],
|
|
31
|
+
"dependencies": {
|
|
32
|
+
"bash-parser": "^0.5.0",
|
|
33
|
+
"yaml": "^2.4.0"
|
|
34
|
+
},
|
|
31
35
|
"devDependencies": {
|
|
32
36
|
"@types/node": "^20.0.0",
|
|
33
|
-
"bash-parser": "^0.5.0",
|
|
34
37
|
"tsup": "^8.0.0",
|
|
35
38
|
"typescript": "^5.4.0",
|
|
36
|
-
"vitest": "^1.6.0"
|
|
37
|
-
"yaml": "^2.4.0"
|
|
39
|
+
"vitest": "^1.6.0"
|
|
38
40
|
},
|
|
39
41
|
"scripts": {
|
|
40
42
|
"build": "tsup",
|