claude-warden 1.0.0 → 1.0.2

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,11 +1,19 @@
1
1
  {
2
2
  "name": "claude-warden",
3
3
  "description": "Smart command safety filter for Claude Code",
4
+ "owner": {
5
+ "name": "banyudu"
6
+ },
4
7
  "plugins": [
5
8
  {
6
9
  "name": "claude-warden",
7
10
  "description": "Auto-approves safe commands, blocks dangerous ones, prompts for the rest",
8
- "source": "."
11
+ "version": "1.0.1",
12
+ "author": {
13
+ "name": "banyudu"
14
+ },
15
+ "source": "./",
16
+ "category": "security"
9
17
  }
10
18
  ]
11
19
  }
package/README.md CHANGED
@@ -2,15 +2,38 @@
2
2
 
3
3
  Smart command safety filter for [Claude Code](https://claude.ai/code). Parses shell commands, evaluates each against configurable safety rules, and returns allow/deny/ask decisions — eliminating unnecessary permission prompts while blocking dangerous commands.
4
4
 
5
- ## What it does
5
+ ## The problem
6
6
 
7
- Without Warden, Claude Code prompts you for **every** shell command. With Warden:
7
+ Claude Code's permission system is all-or-nothing. In the default mode, you're prompted for **every** shell command — even `ls`, `cat`, and `grep`. This creates a painful UX where you're clicking "Allow" hundreds of times per session on obviously safe commands. The alternative (yolo mode) disables all prompts, which is dangerous.
8
8
 
9
- - `ls`, `grep`, `cat`, `git status` **auto-approved** (100+ safe commands)
10
- - `sudo`, `shutdown`, `rm -rf /` → **auto-denied**
11
- - `npm install`, `docker build`, `ssh prod` → **configurable** per-command rules with argument pattern matching
9
+ There's no middle ground: you can't say "allow `git` but block `git push --force`", or "allow `ssh` to my dev server but prompt for production". And compound commands like `npm run build && npm test` trigger a single opaque prompt with no visibility into what's actually being run.
12
10
 
13
- It handles pipes, chains (`&&`, `||`, `;`), env prefixes, `sh -c` wrappers, and subshells. If any command in a pipeline is denied, the whole pipeline is denied.
11
+ ## How Warden solves it
12
+
13
+ Warden hooks into Claude Code's `PreToolUse` event and **parses every shell command into an AST** using [bash-parser](https://github.com/vorpaljs/bash-parser). This means it doesn't just see `npm run build && git push --force` as a single string — it walks the AST to extract each individual command, then evaluates them independently against a configurable rule engine.
14
+
15
+ This AST-based approach enables:
16
+
17
+ - **Pipe and chain decomposition**: `cat file | grep pattern | wc -l` is parsed into three commands, each evaluated separately. All safe → auto-allow. One dangerous → deny the whole pipeline.
18
+ - **Argument-aware rules**: `git status` → allow, `git push --force` → prompt. `rm temp.txt` → allow, `rm -rf /` → deny. The evaluator matches against argument patterns, not just command names.
19
+ - **Recursive evaluation of remote commands**: `ssh devserver 'cat /etc/hosts'` → Warden extracts the remote command, parses it through the same pipeline, and allows it. `ssh devserver 'sudo rm -rf /'` → denied. Same for `docker exec`, `kubectl exec`, and `sprite exec`.
20
+ - **Shell wrapper unwrapping**: `sh -c "npm run build && npm test"` → the inner command is extracted and recursively parsed/evaluated, not treated as an opaque string.
21
+ - **Env prefix handling**: `NODE_ENV=production npm run build` → correctly evaluates `npm run build`, ignoring the env prefix.
22
+ - **Subshell detection**: Commands with `$()` or backticks are flagged for prompting, since their actual content can't be statically evaluated.
23
+
24
+ The result: **100+ common dev commands auto-approved**, dangerous commands auto-denied, everything else configurable — with zero changes to how you use Claude Code.
25
+
26
+ ### Before and after
27
+
28
+ | Command | Without Warden | With Warden |
29
+ |---------|---------------|-------------|
30
+ | `ls -la` | Prompted | Auto-allowed |
31
+ | `cat file \| grep pattern \| wc -l` | Prompted | Auto-allowed (3 safe commands) |
32
+ | `npm run build && npm test` | Prompted | Auto-allowed |
33
+ | `git push --force origin main` | Prompted | Prompted (force push is risky) |
34
+ | `sudo rm -rf /` | Prompted | Auto-denied |
35
+ | `ssh devserver cat /etc/hosts` | Prompted | Auto-allowed (trusted host + safe cmd) |
36
+ | `ssh devserver sudo rm -rf /` | Prompted | Auto-denied (trusted host + dangerous cmd) |
14
37
 
15
38
  ## Install
16
39
 
@@ -114,13 +137,23 @@ File readers (`cat`, `head`, `tail`, `less`), search tools (`grep`, `rg`, `find`
114
137
  ### Conditional rules
115
138
  Commands like `node`, `npx`, `docker`, `ssh`, `git push --force`, `rm` have argument-aware rules. For example, `git` is allowed but `git push --force` triggers a prompt.
116
139
 
140
+ ### Trusted remote targets
141
+ Configure trusted hosts/containers/contexts to auto-allow connections and recursively evaluate remote commands:
142
+ - **SSH**: `trustedSSHHosts` — also covers `scp` and `rsync`
143
+ - **Docker**: `trustedDockerContainers` — for `docker exec`
144
+ - **kubectl**: `trustedKubectlContexts` — for `kubectl exec` (requires explicit `--context`)
145
+ - **Sprite**: `trustedSprites` — for `sprite exec`/`console`
146
+
147
+ All support glob patterns: `*`, `?`, `[...]`, `[!...]`, `{a,b,c}`
148
+
117
149
  ## How it works
118
150
 
119
151
  1. Claude Code calls the `PreToolUse` hook before every Bash command
120
- 2. Warden parses the command into individual parts (handling pipes, chains, env prefixes)
121
- 3. Each part is evaluated: global deny alwaysDeny alwaysAllow command rules default
122
- 4. For pipelines: any deny → deny whole pipeline, any askask, all allowallow
123
- 5. Returns the decision via stdout JSON (allow/ask) or exit code 2 (deny)
152
+ 2. Warden parses the command into an AST via [bash-parser](https://github.com/vorpaljs/bash-parser), walking the tree to extract individual commands from pipes, chains, logical expressions, and subshells
153
+ 3. Shell wrappers (`sh -c`, `bash -c`) and remote commands (`ssh`, `docker exec`, `kubectl exec`, `sprite exec`) are recursively parsed and evaluated
154
+ 4. Each command is evaluated through the rule hierarchy: global deny patterns alwaysDeny alwaysAllow trusted remote targets command-specific rules with argument matching default decision
155
+ 5. Results are combined: any deny deny whole pipeline, any ask ask, all allow → allow
156
+ 6. Returns the decision via stdout JSON (allow/ask) or exit code 2 (deny)
124
157
 
125
158
  ## Development
126
159
 
package/dist/index.cjs CHANGED
@@ -18166,33 +18166,39 @@ function convertCommand(node) {
18166
18166
  const raw = rawParts.join(" ");
18167
18167
  return { command, args: args2, envPrefixes, raw };
18168
18168
  }
18169
- function hasCommandExpansion(node) {
18170
- if (node.type === "CommandExpansion") return true;
18169
+ function collectCommandExpansions(node) {
18170
+ const commands = [];
18171
18171
  if (node.type === "Command") {
18172
18172
  const cmd = node;
18173
18173
  if (cmd.suffix) {
18174
18174
  for (const s of cmd.suffix) {
18175
18175
  if (s.type === "Word" && s.expansion) {
18176
18176
  for (const exp of s.expansion) {
18177
- if (exp.type === "CommandExpansion") return true;
18177
+ if (exp.type === "CommandExpansion" && exp.command) {
18178
+ commands.push(exp.command);
18179
+ }
18178
18180
  }
18179
18181
  }
18180
18182
  }
18181
18183
  }
18182
18184
  if (cmd.name?.expansion) {
18183
18185
  for (const exp of cmd.name.expansion) {
18184
- if (exp.type === "CommandExpansion") return true;
18186
+ if (exp.type === "CommandExpansion" && exp.command) {
18187
+ commands.push(exp.command);
18188
+ }
18185
18189
  }
18186
18190
  }
18187
18191
  }
18188
- return false;
18192
+ return commands;
18189
18193
  }
18190
18194
  function walkNode(node, result) {
18191
18195
  switch (node.type) {
18192
18196
  case "Command": {
18193
18197
  const cmd = node;
18194
- if (hasCommandExpansion(node)) {
18198
+ const expansions = collectCommandExpansions(node);
18199
+ if (expansions.length > 0) {
18195
18200
  result.hasSubshell = true;
18201
+ result.subshellCommands.push(...expansions);
18196
18202
  }
18197
18203
  const parsed = convertCommand(cmd);
18198
18204
  if (!parsed) break;
@@ -18205,6 +18211,7 @@ function walkNode(node, result) {
18205
18211
  if (innerResult.hasSubshell) {
18206
18212
  result.hasSubshell = true;
18207
18213
  }
18214
+ result.subshellCommands.push(...innerResult.subshellCommands);
18208
18215
  }
18209
18216
  } else {
18210
18217
  result.commands.push(parsed);
@@ -18249,35 +18256,35 @@ function walkNode(node, result) {
18249
18256
  }
18250
18257
  function parseCommand(input) {
18251
18258
  if (!input || !input.trim()) {
18252
- return { commands: [], hasSubshell: false, parseError: false };
18259
+ return { commands: [], hasSubshell: false, subshellCommands: [], parseError: false };
18253
18260
  }
18254
18261
  const hasHeredoc = HEREDOC_REGEX.test(input);
18255
18262
  if (hasHeredoc) {
18256
18263
  const firstLine = input.split("\n")[0];
18257
18264
  const cmdPart = firstLine.replace(/<<-?\s*['"]?\w+['"]?.*$/, "").trim();
18258
18265
  if (!cmdPart) {
18259
- return { commands: [], hasSubshell: false, parseError: true };
18266
+ return { commands: [], hasSubshell: false, subshellCommands: [], parseError: true };
18260
18267
  }
18261
18268
  try {
18262
18269
  const ast = (0, import_bash_parser.default)(cmdPart);
18263
- const result = { commands: [], hasSubshell: false };
18270
+ const result = { commands: [], hasSubshell: false, subshellCommands: [] };
18264
18271
  for (const cmd of ast.commands) {
18265
18272
  walkNode(cmd, result);
18266
18273
  }
18267
- return { commands: result.commands, hasSubshell: true, parseError: false };
18274
+ return { commands: result.commands, hasSubshell: true, subshellCommands: result.subshellCommands, parseError: false };
18268
18275
  } catch {
18269
- return { commands: [], hasSubshell: true, parseError: true };
18276
+ return { commands: [], hasSubshell: true, subshellCommands: [], parseError: true };
18270
18277
  }
18271
18278
  }
18272
18279
  try {
18273
18280
  const ast = (0, import_bash_parser.default)(input);
18274
- const result = { commands: [], hasSubshell: false };
18281
+ const result = { commands: [], hasSubshell: false, subshellCommands: [] };
18275
18282
  for (const cmd of ast.commands) {
18276
18283
  walkNode(cmd, result);
18277
18284
  }
18278
- return { commands: result.commands, hasSubshell: result.hasSubshell, parseError: false };
18285
+ return { commands: result.commands, hasSubshell: result.hasSubshell, subshellCommands: result.subshellCommands, parseError: false };
18279
18286
  } catch {
18280
- return { commands: [], hasSubshell: false, parseError: true };
18287
+ return { commands: [], hasSubshell: false, subshellCommands: [], parseError: true };
18281
18288
  }
18282
18289
  }
18283
18290
 
@@ -18289,7 +18296,18 @@ function evaluate(parsed, config) {
18289
18296
  if (parsed.commands.length === 0) {
18290
18297
  return { decision: "allow", reason: "Empty command", details: [] };
18291
18298
  }
18292
- if (parsed.hasSubshell && config.askOnSubshell) {
18299
+ if (parsed.hasSubshell && parsed.subshellCommands.length > 0) {
18300
+ for (const subCmd of parsed.subshellCommands) {
18301
+ const subParsed = parseCommand(subCmd);
18302
+ const subResult = evaluate(subParsed, config);
18303
+ if (subResult.decision === "deny") {
18304
+ return { decision: "deny", reason: `Subshell command: ${subResult.reason}`, details: subResult.details };
18305
+ }
18306
+ if (subResult.decision === "ask") {
18307
+ return { decision: "ask", reason: `Subshell command: ${subResult.reason}`, details: subResult.details };
18308
+ }
18309
+ }
18310
+ } else if (parsed.hasSubshell && parsed.subshellCommands.length === 0 && config.askOnSubshell) {
18293
18311
  return { decision: "ask", reason: "Command contains subshell/command substitution", details: [] };
18294
18312
  }
18295
18313
  const details = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-warden",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
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",