claude-warden 1.2.0 → 1.3.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/README.md +2 -2
- package/config/warden.default.yaml +23 -7
- package/dist/index.cjs +90 -40
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-warden",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.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/README.md
CHANGED
|
@@ -127,14 +127,14 @@ rules:
|
|
|
127
127
|
description: Read-only docker commands
|
|
128
128
|
```
|
|
129
129
|
|
|
130
|
-
## Feedback and `/warden-allow`
|
|
130
|
+
## Feedback and `/claude-warden:warden-allow`
|
|
131
131
|
|
|
132
132
|
When Warden blocks or flags a command, it includes a system message explaining:
|
|
133
133
|
|
|
134
134
|
1. **Why** the command was blocked/flagged (per-command reasons)
|
|
135
135
|
2. **How to allow it** — a ready-to-use YAML snippet for your config
|
|
136
136
|
|
|
137
|
-
Use the `/warden-allow` slash command to apply the suggested config change. It will ask which scope (project or user) to use.
|
|
137
|
+
Use the `/claude-warden:warden-allow` slash command to apply the suggested config change. It will ask which scope (project or user) to use.
|
|
138
138
|
|
|
139
139
|
## Built-in defaults
|
|
140
140
|
|
|
@@ -27,29 +27,45 @@ askOnSubshell: true
|
|
|
27
27
|
# Trusted SSH hosts — ssh/scp/rsync to these hosts are auto-allowed.
|
|
28
28
|
# Remote commands on trusted hosts are recursively evaluated through warden rules.
|
|
29
29
|
# Supports glob patterns (* wildcards).
|
|
30
|
+
# Entries can be strings (use global overrides) or objects with per-target settings.
|
|
30
31
|
# trustedSSHHosts:
|
|
31
|
-
# - devserver
|
|
32
|
+
# - devserver # string → uses global overrides only
|
|
32
33
|
# - staging-*
|
|
33
34
|
# - "*.internal.company.com"
|
|
34
|
-
# -
|
|
35
|
+
# - name: prod-bastion # object with per-target overrides
|
|
36
|
+
# overrides:
|
|
37
|
+
# alwaysAllow: [systemctl]
|
|
35
38
|
|
|
36
39
|
# Trusted Docker containers — docker exec to these containers are auto-allowed.
|
|
37
40
|
# Remote commands are recursively evaluated through warden rules.
|
|
38
41
|
# trustedDockerContainers:
|
|
39
|
-
# - my-app
|
|
40
|
-
# - dev
|
|
42
|
+
# - my-app # string → uses global overrides only
|
|
43
|
+
# - name: dev-container # object with allowAll (skip all checks)
|
|
44
|
+
# allowAll: true
|
|
45
|
+
# - name: staging-* # object with per-target overrides
|
|
46
|
+
# overrides:
|
|
47
|
+
# alwaysAllow: [sudo, apt]
|
|
41
48
|
|
|
42
49
|
# Trusted kubectl contexts — kubectl exec in these contexts are auto-allowed.
|
|
43
50
|
# Remote commands (after --) are recursively evaluated through warden rules.
|
|
44
51
|
# trustedKubectlContexts:
|
|
45
52
|
# - minikube
|
|
46
|
-
# - dev-cluster-*
|
|
53
|
+
# - name: dev-cluster-*
|
|
54
|
+
# overrides:
|
|
55
|
+
# alwaysAllow: [sudo]
|
|
56
|
+
# - name: prod-cluster
|
|
57
|
+
# overrides:
|
|
58
|
+
# alwaysDeny: [rm]
|
|
47
59
|
|
|
48
60
|
# Trusted Sprites — sprite exec/console to these sprites are auto-allowed.
|
|
49
61
|
# Remote commands are recursively evaluated through warden rules.
|
|
50
62
|
# trustedSprites:
|
|
51
|
-
# - my-sprite
|
|
52
|
-
# -
|
|
63
|
+
# - my-sprite # string → uses global overrides only
|
|
64
|
+
# - name: yudu-claw # allowAll: skip all checks (disposable env)
|
|
65
|
+
# allowAll: true
|
|
66
|
+
# - name: dev-* # per-target overrides
|
|
67
|
+
# overrides:
|
|
68
|
+
# alwaysAllow: [sudo, apt]
|
|
53
69
|
|
|
54
70
|
# Override rules when evaluating commands inside trusted remote contexts
|
|
55
71
|
# (docker exec, kubectl exec, ssh, sprite exec). These overrides are applied
|
package/dist/index.cjs
CHANGED
|
@@ -18515,8 +18515,8 @@ function globToRegex(pattern) {
|
|
|
18515
18515
|
}
|
|
18516
18516
|
return new RegExp(`^${regex}$`);
|
|
18517
18517
|
}
|
|
18518
|
-
function
|
|
18519
|
-
return
|
|
18518
|
+
function findMatchingTarget(value, targets) {
|
|
18519
|
+
return targets.find((t) => globToRegex(t.name).test(value)) || null;
|
|
18520
18520
|
}
|
|
18521
18521
|
function parseSSHArgs(args2) {
|
|
18522
18522
|
let host = null;
|
|
@@ -18560,19 +18560,21 @@ function evaluateSSHCommand(cmd, config) {
|
|
|
18560
18560
|
const trustedHosts = config.trustedSSHHosts || [];
|
|
18561
18561
|
if (command === "scp" || command === "rsync") {
|
|
18562
18562
|
const host2 = extractHostFromRemotePath(args2);
|
|
18563
|
-
if (host2
|
|
18564
|
-
|
|
18565
|
-
|
|
18566
|
-
|
|
18567
|
-
|
|
18568
|
-
|
|
18569
|
-
|
|
18570
|
-
}
|
|
18571
|
-
|
|
18572
|
-
|
|
18563
|
+
if (!host2) return null;
|
|
18564
|
+
const target2 = findMatchingTarget(host2, trustedHosts);
|
|
18565
|
+
if (!target2) return null;
|
|
18566
|
+
return {
|
|
18567
|
+
command,
|
|
18568
|
+
args: args2,
|
|
18569
|
+
decision: "allow",
|
|
18570
|
+
reason: `Trusted SSH host "${host2}"`,
|
|
18571
|
+
matchedRule: "trustedSSHHosts"
|
|
18572
|
+
};
|
|
18573
18573
|
}
|
|
18574
18574
|
const { host, remoteCommand } = parseSSHArgs(args2);
|
|
18575
|
-
if (!host
|
|
18575
|
+
if (!host) return null;
|
|
18576
|
+
const target = findMatchingTarget(host, trustedHosts);
|
|
18577
|
+
if (!target) return null;
|
|
18576
18578
|
if (!remoteCommand) {
|
|
18577
18579
|
return {
|
|
18578
18580
|
command,
|
|
@@ -18582,8 +18584,17 @@ function evaluateSSHCommand(cmd, config) {
|
|
|
18582
18584
|
matchedRule: "trustedSSHHosts"
|
|
18583
18585
|
};
|
|
18584
18586
|
}
|
|
18587
|
+
if (target.allowAll) {
|
|
18588
|
+
return {
|
|
18589
|
+
command,
|
|
18590
|
+
args: args2,
|
|
18591
|
+
decision: "allow",
|
|
18592
|
+
reason: `Trusted SSH host "${host}" (allowAll)`,
|
|
18593
|
+
matchedRule: "trustedSSHHosts"
|
|
18594
|
+
};
|
|
18595
|
+
}
|
|
18585
18596
|
const parsed = parseCommand(remoteCommand);
|
|
18586
|
-
const result = evaluate(parsed, configWithContextOverrides(config));
|
|
18597
|
+
const result = evaluate(parsed, configWithContextOverrides(config, target));
|
|
18587
18598
|
return {
|
|
18588
18599
|
command,
|
|
18589
18600
|
args: args2,
|
|
@@ -18603,15 +18614,21 @@ var DOCKER_EXEC_FLAGS_WITH_VALUE = /* @__PURE__ */ new Set([
|
|
|
18603
18614
|
"--detach-keys"
|
|
18604
18615
|
]);
|
|
18605
18616
|
var INTERACTIVE_SHELLS = /* @__PURE__ */ new Set(["bash", "sh", "zsh"]);
|
|
18606
|
-
function configWithContextOverrides(config) {
|
|
18607
|
-
|
|
18617
|
+
function configWithContextOverrides(config, target) {
|
|
18618
|
+
const overrideLayers = [];
|
|
18619
|
+
if (target?.overrides) overrideLayers.push(target.overrides);
|
|
18620
|
+
if (config.trustedContextOverrides) overrideLayers.push(config.trustedContextOverrides);
|
|
18621
|
+
if (overrideLayers.length === 0) return config;
|
|
18608
18622
|
return {
|
|
18609
18623
|
...config,
|
|
18610
|
-
layers: [
|
|
18624
|
+
layers: [...overrideLayers, ...config.layers]
|
|
18611
18625
|
};
|
|
18612
18626
|
}
|
|
18613
|
-
function evaluateRemoteCommand(remoteArgs, config) {
|
|
18614
|
-
|
|
18627
|
+
function evaluateRemoteCommand(remoteArgs, config, target) {
|
|
18628
|
+
if (target?.allowAll) {
|
|
18629
|
+
return { decision: "allow", reason: "allowAll target", details: [] };
|
|
18630
|
+
}
|
|
18631
|
+
const overriddenConfig = configWithContextOverrides(config, target);
|
|
18615
18632
|
if (remoteArgs.length === 0) {
|
|
18616
18633
|
return { decision: "allow", reason: "interactive", details: [] };
|
|
18617
18634
|
}
|
|
@@ -18625,9 +18642,10 @@ function evaluateRemoteCommand(remoteArgs, config) {
|
|
|
18625
18642
|
return evaluate(parsed2, overriddenConfig);
|
|
18626
18643
|
}
|
|
18627
18644
|
const parsed = {
|
|
18628
|
-
commands: [{ command: remoteCmd, args: remoteArgs.slice(1) }],
|
|
18645
|
+
commands: [{ command: remoteCmd, args: remoteArgs.slice(1), envPrefixes: [], raw: remoteArgs.join(" ") }],
|
|
18629
18646
|
hasSubshell: false,
|
|
18630
|
-
subshellCommands: []
|
|
18647
|
+
subshellCommands: [],
|
|
18648
|
+
parseError: false
|
|
18631
18649
|
};
|
|
18632
18650
|
return evaluate(parsed, overriddenConfig);
|
|
18633
18651
|
}
|
|
@@ -18661,14 +18679,16 @@ function parseDockerExecArgs(args2) {
|
|
|
18661
18679
|
function evaluateDockerExec(cmd, config) {
|
|
18662
18680
|
const { command, args: args2 } = cmd;
|
|
18663
18681
|
if (args2[0] !== "exec") return null;
|
|
18664
|
-
const { target, remoteArgs } = parseDockerExecArgs(args2.slice(1));
|
|
18665
|
-
if (!
|
|
18666
|
-
const
|
|
18682
|
+
const { target: containerName, remoteArgs } = parseDockerExecArgs(args2.slice(1));
|
|
18683
|
+
if (!containerName) return null;
|
|
18684
|
+
const matched = findMatchingTarget(containerName, config.trustedDockerContainers || []);
|
|
18685
|
+
if (!matched) return null;
|
|
18686
|
+
const result = evaluateRemoteCommand(remoteArgs, config, matched);
|
|
18667
18687
|
return {
|
|
18668
18688
|
command,
|
|
18669
18689
|
args: args2,
|
|
18670
18690
|
decision: result.decision,
|
|
18671
|
-
reason: `Trusted Docker container "${
|
|
18691
|
+
reason: `Trusted Docker container "${containerName}" (${result.reason})`,
|
|
18672
18692
|
matchedRule: "trustedDockerContainers"
|
|
18673
18693
|
};
|
|
18674
18694
|
}
|
|
@@ -18740,9 +18760,10 @@ function evaluateKubectlExec(cmd, config) {
|
|
|
18740
18760
|
const { command, args: args2 } = cmd;
|
|
18741
18761
|
if (args2[0] !== "exec") return null;
|
|
18742
18762
|
const { context, pod, remoteArgs } = parseKubectlExecArgs(args2.slice(1));
|
|
18743
|
-
|
|
18744
|
-
|
|
18745
|
-
|
|
18763
|
+
if (!context) return null;
|
|
18764
|
+
const matched = findMatchingTarget(context, config.trustedKubectlContexts || []);
|
|
18765
|
+
if (!matched) return null;
|
|
18766
|
+
const result = evaluateRemoteCommand(remoteArgs, config, matched);
|
|
18746
18767
|
return {
|
|
18747
18768
|
command,
|
|
18748
18769
|
args: args2,
|
|
@@ -18805,9 +18826,10 @@ function parseSpriteExecArgs(args2) {
|
|
|
18805
18826
|
function evaluateSpriteExec(cmd, config) {
|
|
18806
18827
|
const { command, args: args2 } = cmd;
|
|
18807
18828
|
const { spriteName, remoteArgs } = parseSpriteExecArgs(args2);
|
|
18808
|
-
|
|
18809
|
-
|
|
18810
|
-
|
|
18829
|
+
if (!spriteName) return null;
|
|
18830
|
+
const matched = findMatchingTarget(spriteName, config.trustedSprites || []);
|
|
18831
|
+
if (!matched) return null;
|
|
18832
|
+
const result = evaluateRemoteCommand(remoteArgs, config, matched);
|
|
18811
18833
|
return {
|
|
18812
18834
|
command,
|
|
18813
18835
|
args: args2,
|
|
@@ -18904,7 +18926,8 @@ var SAFE_PKG_MANAGER_CMDS = [
|
|
|
18904
18926
|
"dedupe",
|
|
18905
18927
|
"prune",
|
|
18906
18928
|
"audit",
|
|
18907
|
-
"completion"
|
|
18929
|
+
"completion",
|
|
18930
|
+
"whoami"
|
|
18908
18931
|
];
|
|
18909
18932
|
var VERSION_HELP_FLAGS = {
|
|
18910
18933
|
match: { anyArgMatches: ["^--(version|help)$", "^-[vh]$"] },
|
|
@@ -19057,6 +19080,18 @@ var DEFAULT_CONFIG = {
|
|
|
19057
19080
|
"yq",
|
|
19058
19081
|
"xargs",
|
|
19059
19082
|
"seq",
|
|
19083
|
+
// Network diagnostics (read-only)
|
|
19084
|
+
"nslookup",
|
|
19085
|
+
"dig",
|
|
19086
|
+
"host",
|
|
19087
|
+
"ping",
|
|
19088
|
+
"traceroute",
|
|
19089
|
+
"mtr",
|
|
19090
|
+
"netstat",
|
|
19091
|
+
"ss",
|
|
19092
|
+
"ifconfig",
|
|
19093
|
+
"ip",
|
|
19094
|
+
"nmap",
|
|
19060
19095
|
// Pagers and formatters
|
|
19061
19096
|
"bat",
|
|
19062
19097
|
"pygmentize",
|
|
@@ -19334,18 +19369,33 @@ function extractLayer(raw) {
|
|
|
19334
19369
|
rules: Array.isArray(raw.rules) ? raw.rules : []
|
|
19335
19370
|
};
|
|
19336
19371
|
}
|
|
19372
|
+
function parseTrustedList(raw) {
|
|
19373
|
+
return raw.map((entry) => {
|
|
19374
|
+
if (typeof entry === "string") return { name: entry };
|
|
19375
|
+
if (entry && typeof entry === "object" && "name" in entry) {
|
|
19376
|
+
const obj = entry;
|
|
19377
|
+
const target = { name: String(obj.name) };
|
|
19378
|
+
if (obj.allowAll === true) target.allowAll = true;
|
|
19379
|
+
if (obj.overrides && typeof obj.overrides === "object") {
|
|
19380
|
+
target.overrides = extractLayer(obj.overrides);
|
|
19381
|
+
}
|
|
19382
|
+
return target;
|
|
19383
|
+
}
|
|
19384
|
+
return null;
|
|
19385
|
+
}).filter((t) => t !== null);
|
|
19386
|
+
}
|
|
19337
19387
|
function mergeNonLayerFields(config, raw) {
|
|
19338
19388
|
if (Array.isArray(raw.trustedSSHHosts)) {
|
|
19339
|
-
config.trustedSSHHosts = [...config.trustedSSHHosts || [], ...raw.trustedSSHHosts];
|
|
19389
|
+
config.trustedSSHHosts = [...config.trustedSSHHosts || [], ...parseTrustedList(raw.trustedSSHHosts)];
|
|
19340
19390
|
}
|
|
19341
19391
|
if (Array.isArray(raw.trustedDockerContainers)) {
|
|
19342
|
-
config.trustedDockerContainers = [...config.trustedDockerContainers || [], ...raw.trustedDockerContainers];
|
|
19392
|
+
config.trustedDockerContainers = [...config.trustedDockerContainers || [], ...parseTrustedList(raw.trustedDockerContainers)];
|
|
19343
19393
|
}
|
|
19344
19394
|
if (Array.isArray(raw.trustedKubectlContexts)) {
|
|
19345
|
-
config.trustedKubectlContexts = [...config.trustedKubectlContexts || [], ...raw.trustedKubectlContexts];
|
|
19395
|
+
config.trustedKubectlContexts = [...config.trustedKubectlContexts || [], ...parseTrustedList(raw.trustedKubectlContexts)];
|
|
19346
19396
|
}
|
|
19347
19397
|
if (Array.isArray(raw.trustedSprites)) {
|
|
19348
|
-
config.trustedSprites = [...config.trustedSprites || [], ...raw.trustedSprites];
|
|
19398
|
+
config.trustedSprites = [...config.trustedSprites || [], ...parseTrustedList(raw.trustedSprites)];
|
|
19349
19399
|
}
|
|
19350
19400
|
if (typeof raw.defaultDecision === "string") {
|
|
19351
19401
|
config.defaultDecision = raw.defaultDecision;
|
|
@@ -19407,15 +19457,15 @@ function formatSystemMessage(decision, rawCommand, details) {
|
|
|
19407
19457
|
const header = `[warden] ${parts.join(" | ")}`;
|
|
19408
19458
|
const subcommandHints = relevant.filter((d) => d.args.length > 0).map((d) => {
|
|
19409
19459
|
const sub = d.args[0];
|
|
19410
|
-
return ` Option A: Allow all \`${d.command}\` \u2192 \`/warden-allow ${d.command}\`
|
|
19411
|
-
Option B: Allow only \`${d.command} ${sub}\` \u2192 \`/warden-allow ${d.command} ${sub}\``;
|
|
19460
|
+
return ` Option A: Allow all \`${d.command}\` \u2192 \`/claude-warden:warden-allow ${d.command}\`
|
|
19461
|
+
Option B: Allow only \`${d.command} ${sub}\` \u2192 \`/claude-warden:warden-allow ${d.command} ${sub}\``;
|
|
19412
19462
|
});
|
|
19413
19463
|
if (subcommandHints.length > 0) {
|
|
19414
19464
|
return `${header}
|
|
19415
19465
|
${subcommandHints.join("\n")}
|
|
19416
|
-
See /warden-allow`;
|
|
19466
|
+
See /claude-warden:warden-allow`;
|
|
19417
19467
|
}
|
|
19418
|
-
return `${header} \u2014 To auto-allow, see /warden-allow`;
|
|
19468
|
+
return `${header} \u2014 To auto-allow, see /claude-warden:warden-allow`;
|
|
19419
19469
|
}
|
|
19420
19470
|
const lines = ["[warden] Command blocked", ""];
|
|
19421
19471
|
if (relevant.length > 0) {
|