claude-warden 1.4.0 → 1.5.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.
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +6 -0
- package/config/warden.default.yaml +6 -0
- package/dist/index.cjs +95 -9
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "claude-warden",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.2",
|
|
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
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Claude Warden
|
|
2
2
|
|
|
3
|
+
[](https://www.npmjs.com/package/claude-warden)
|
|
4
|
+
[](https://www.npmjs.com/package/claude-warden)
|
|
5
|
+
[](https://github.com/banyudu/claude-warden/blob/main/LICENSE)
|
|
6
|
+
[](https://github.com/banyudu/claude-warden/stargazers)
|
|
7
|
+
[](https://github.com/banyudu/claude-warden/actions)
|
|
8
|
+
|
|
3
9
|
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
10
|
|
|
5
11
|
## The problem
|
|
@@ -13,6 +13,12 @@ defaultDecision: ask
|
|
|
13
13
|
# If true, commands containing $() or backticks trigger "ask"
|
|
14
14
|
askOnSubshell: true
|
|
15
15
|
|
|
16
|
+
# Send OS notifications when warden prompts for permission or blocks a command.
|
|
17
|
+
# On macOS, uses terminal-notifier (click to activate terminal) or osascript fallback.
|
|
18
|
+
# On Linux, uses notify-send.
|
|
19
|
+
notifyOnAsk: true
|
|
20
|
+
notifyOnDeny: true
|
|
21
|
+
|
|
16
22
|
# Additional commands to always allow (checked after alwaysDeny within this scope)
|
|
17
23
|
# alwaysAllow:
|
|
18
24
|
# - terraform
|
package/dist/index.cjs
CHANGED
|
@@ -18145,7 +18145,8 @@ function preprocessCatHeredocs(input) {
|
|
|
18145
18145
|
}
|
|
18146
18146
|
function convertCommand(node) {
|
|
18147
18147
|
if (!node.name) return null;
|
|
18148
|
-
const
|
|
18148
|
+
const originalCommand = node.name.text;
|
|
18149
|
+
const command = originalCommand.includes("/") ? (0, import_path.basename)(originalCommand) : originalCommand;
|
|
18149
18150
|
const envPrefixes = [];
|
|
18150
18151
|
if (node.prefix) {
|
|
18151
18152
|
for (const p of node.prefix) {
|
|
@@ -18168,7 +18169,7 @@ function convertCommand(node) {
|
|
|
18168
18169
|
...args2
|
|
18169
18170
|
];
|
|
18170
18171
|
const raw = rawParts.join(" ");
|
|
18171
|
-
return { command, args: args2, envPrefixes, raw };
|
|
18172
|
+
return { command, originalCommand, args: args2, envPrefixes, raw };
|
|
18172
18173
|
}
|
|
18173
18174
|
function collectCommandExpansions(node) {
|
|
18174
18175
|
const commands = [];
|
|
@@ -18335,6 +18336,16 @@ function parseCommand(input) {
|
|
|
18335
18336
|
}
|
|
18336
18337
|
|
|
18337
18338
|
// src/evaluator.ts
|
|
18339
|
+
var import_os = require("os");
|
|
18340
|
+
function commandMatchesName(cmd, name) {
|
|
18341
|
+
if (name.startsWith("/")) {
|
|
18342
|
+
return cmd.originalCommand === name;
|
|
18343
|
+
}
|
|
18344
|
+
if (name.startsWith("~/")) {
|
|
18345
|
+
return cmd.originalCommand === (0, import_os.homedir)() + name.slice(1);
|
|
18346
|
+
}
|
|
18347
|
+
return cmd.command === name;
|
|
18348
|
+
}
|
|
18338
18349
|
function evaluate(parsed, config) {
|
|
18339
18350
|
if (parsed.parseError) {
|
|
18340
18351
|
return { decision: "ask", reason: "Could not parse command safely", details: [] };
|
|
@@ -18382,10 +18393,10 @@ function evaluate(parsed, config) {
|
|
|
18382
18393
|
function evaluateCommand(cmd, config) {
|
|
18383
18394
|
const { command, args: args2 } = cmd;
|
|
18384
18395
|
for (const layer of config.layers) {
|
|
18385
|
-
if (layer.alwaysDeny.
|
|
18396
|
+
if (layer.alwaysDeny.some((name) => commandMatchesName(cmd, name))) {
|
|
18386
18397
|
return { command, args: args2, decision: "deny", reason: `"${command}" is blocked`, matchedRule: "alwaysDeny" };
|
|
18387
18398
|
}
|
|
18388
|
-
if (layer.alwaysAllow.
|
|
18399
|
+
if (layer.alwaysAllow.some((name) => commandMatchesName(cmd, name))) {
|
|
18389
18400
|
return { command, args: args2, decision: "allow", reason: `"${command}" is safe`, matchedRule: "alwaysAllow" };
|
|
18390
18401
|
}
|
|
18391
18402
|
}
|
|
@@ -18406,7 +18417,7 @@ function evaluateCommand(cmd, config) {
|
|
|
18406
18417
|
if (spriteResult) return spriteResult;
|
|
18407
18418
|
}
|
|
18408
18419
|
for (const layer of config.layers) {
|
|
18409
|
-
const rule = layer.rules.find((r) => r.command
|
|
18420
|
+
const rule = layer.rules.find((r) => commandMatchesName(cmd, r.command));
|
|
18410
18421
|
if (rule) {
|
|
18411
18422
|
return evaluateRule(cmd, rule);
|
|
18412
18423
|
}
|
|
@@ -18642,7 +18653,7 @@ function evaluateRemoteCommand(remoteArgs, config, target) {
|
|
|
18642
18653
|
return evaluate(parsed2, overriddenConfig);
|
|
18643
18654
|
}
|
|
18644
18655
|
const parsed = {
|
|
18645
|
-
commands: [{ command: remoteCmd, args: remoteArgs.slice(1), envPrefixes: [], raw: remoteArgs.join(" ") }],
|
|
18656
|
+
commands: [{ command: remoteCmd, originalCommand: remoteCmd, args: remoteArgs.slice(1), envPrefixes: [], raw: remoteArgs.join(" ") }],
|
|
18646
18657
|
hasSubshell: false,
|
|
18647
18658
|
subshellCommands: [],
|
|
18648
18659
|
parseError: false
|
|
@@ -18842,7 +18853,7 @@ function evaluateSpriteExec(cmd, config) {
|
|
|
18842
18853
|
// src/rules.ts
|
|
18843
18854
|
var import_fs = require("fs");
|
|
18844
18855
|
var import_yaml = __toESM(require_dist2(), 1);
|
|
18845
|
-
var
|
|
18856
|
+
var import_os2 = require("os");
|
|
18846
18857
|
var import_path2 = require("path");
|
|
18847
18858
|
|
|
18848
18859
|
// src/defaults.ts
|
|
@@ -18988,6 +18999,8 @@ function pkgRunnerRule(command) {
|
|
|
18988
18999
|
var DEFAULT_CONFIG = {
|
|
18989
19000
|
defaultDecision: "ask",
|
|
18990
19001
|
askOnSubshell: true,
|
|
19002
|
+
notifyOnAsk: true,
|
|
19003
|
+
notifyOnDeny: true,
|
|
18991
19004
|
trustedSSHHosts: [],
|
|
18992
19005
|
trustedDockerContainers: [],
|
|
18993
19006
|
trustedKubectlContexts: [],
|
|
@@ -19484,8 +19497,8 @@ var DEFAULT_CONFIG = {
|
|
|
19484
19497
|
|
|
19485
19498
|
// src/rules.ts
|
|
19486
19499
|
var USER_CONFIG_PATHS = [
|
|
19487
|
-
(0, import_path2.join)((0,
|
|
19488
|
-
(0, import_path2.join)((0,
|
|
19500
|
+
(0, import_path2.join)((0, import_os2.homedir)(), ".claude", "warden.yaml"),
|
|
19501
|
+
(0, import_path2.join)((0, import_os2.homedir)(), ".claude", "warden.json")
|
|
19489
19502
|
];
|
|
19490
19503
|
var PROJECT_CONFIG_NAMES = [
|
|
19491
19504
|
".claude/warden.yaml",
|
|
@@ -19578,6 +19591,12 @@ function mergeNonLayerFields(config, raw) {
|
|
|
19578
19591
|
if (typeof raw.askOnSubshell === "boolean") {
|
|
19579
19592
|
config.askOnSubshell = raw.askOnSubshell;
|
|
19580
19593
|
}
|
|
19594
|
+
if (typeof raw.notifyOnAsk === "boolean") {
|
|
19595
|
+
config.notifyOnAsk = raw.notifyOnAsk;
|
|
19596
|
+
}
|
|
19597
|
+
if (typeof raw.notifyOnDeny === "boolean") {
|
|
19598
|
+
config.notifyOnDeny = raw.notifyOnDeny;
|
|
19599
|
+
}
|
|
19581
19600
|
if (raw.trustedContextOverrides && typeof raw.trustedContextOverrides === "object") {
|
|
19582
19601
|
const overrides = raw.trustedContextOverrides;
|
|
19583
19602
|
const layer = extractLayer(overrides);
|
|
@@ -19666,6 +19685,62 @@ See /claude-warden:warden-allow`;
|
|
|
19666
19685
|
return lines.join("\n");
|
|
19667
19686
|
}
|
|
19668
19687
|
|
|
19688
|
+
// src/notify.ts
|
|
19689
|
+
var import_child_process = require("child_process");
|
|
19690
|
+
var TERMINAL_BUNDLE_IDS = {
|
|
19691
|
+
"iTerm.app": "com.googlecode.iterm2",
|
|
19692
|
+
"Apple_Terminal": "com.apple.Terminal",
|
|
19693
|
+
"Alacritty": "com.github.alacritty.Alacritty",
|
|
19694
|
+
"WezTerm": "io.wezfurlong.wezterm"
|
|
19695
|
+
};
|
|
19696
|
+
var terminalNotifierAvailable = null;
|
|
19697
|
+
function hasTerminalNotifier() {
|
|
19698
|
+
if (terminalNotifierAvailable !== null) return terminalNotifierAvailable;
|
|
19699
|
+
try {
|
|
19700
|
+
(0, import_child_process.execFileSync)("which", ["terminal-notifier"], { stdio: "ignore" });
|
|
19701
|
+
terminalNotifierAvailable = true;
|
|
19702
|
+
} catch {
|
|
19703
|
+
terminalNotifierAvailable = false;
|
|
19704
|
+
}
|
|
19705
|
+
return terminalNotifierAvailable;
|
|
19706
|
+
}
|
|
19707
|
+
function getBundleId() {
|
|
19708
|
+
const termProgram = process.env.TERM_PROGRAM;
|
|
19709
|
+
if (!termProgram) return void 0;
|
|
19710
|
+
return TERMINAL_BUNDLE_IDS[termProgram];
|
|
19711
|
+
}
|
|
19712
|
+
function buildNotifyCommand(title, message) {
|
|
19713
|
+
const platform = process.platform;
|
|
19714
|
+
if (platform === "darwin") {
|
|
19715
|
+
if (hasTerminalNotifier()) {
|
|
19716
|
+
const args2 = ["-title", title, "-message", message];
|
|
19717
|
+
const bundleId = getBundleId();
|
|
19718
|
+
if (bundleId) {
|
|
19719
|
+
args2.push("-activate", bundleId);
|
|
19720
|
+
}
|
|
19721
|
+
return { cmd: "terminal-notifier", args: args2 };
|
|
19722
|
+
}
|
|
19723
|
+
const script = `display notification ${JSON.stringify(message)} with title ${JSON.stringify(title)}`;
|
|
19724
|
+
return { cmd: "osascript", args: ["-e", script] };
|
|
19725
|
+
}
|
|
19726
|
+
if (platform === "linux") {
|
|
19727
|
+
return { cmd: "notify-send", args: [title, message] };
|
|
19728
|
+
}
|
|
19729
|
+
return null;
|
|
19730
|
+
}
|
|
19731
|
+
function sendNotification(title, message, config) {
|
|
19732
|
+
try {
|
|
19733
|
+
const notifyCmd = buildNotifyCommand(title, message);
|
|
19734
|
+
if (!notifyCmd) return;
|
|
19735
|
+
const child = (0, import_child_process.spawn)(notifyCmd.cmd, notifyCmd.args, {
|
|
19736
|
+
detached: true,
|
|
19737
|
+
stdio: "ignore"
|
|
19738
|
+
});
|
|
19739
|
+
child.unref();
|
|
19740
|
+
} catch {
|
|
19741
|
+
}
|
|
19742
|
+
}
|
|
19743
|
+
|
|
19669
19744
|
// src/index.ts
|
|
19670
19745
|
async function main() {
|
|
19671
19746
|
let raw = "";
|
|
@@ -19681,6 +19756,9 @@ async function main() {
|
|
|
19681
19756
|
if (input.tool_name !== "Bash") {
|
|
19682
19757
|
process.exit(0);
|
|
19683
19758
|
}
|
|
19759
|
+
if (input.permission_mode === "dangerously-skip-permissions") {
|
|
19760
|
+
process.exit(0);
|
|
19761
|
+
}
|
|
19684
19762
|
const command = input.tool_input?.command;
|
|
19685
19763
|
if (!command || typeof command !== "string") {
|
|
19686
19764
|
process.exit(0);
|
|
@@ -19700,6 +19778,10 @@ async function main() {
|
|
|
19700
19778
|
process.exit(0);
|
|
19701
19779
|
}
|
|
19702
19780
|
if (result.decision === "deny") {
|
|
19781
|
+
if (config.notifyOnDeny) {
|
|
19782
|
+
const truncated = command.length > 80 ? command.slice(0, 77) + "..." : command;
|
|
19783
|
+
sendNotification("Claude Warden", `Blocked: ${truncated}`, config);
|
|
19784
|
+
}
|
|
19703
19785
|
const msg2 = formatSystemMessage("deny", command, result.details);
|
|
19704
19786
|
const output2 = {
|
|
19705
19787
|
hookSpecificOutput: {
|
|
@@ -19713,6 +19795,10 @@ async function main() {
|
|
|
19713
19795
|
`);
|
|
19714
19796
|
process.exit(2);
|
|
19715
19797
|
}
|
|
19798
|
+
if (config.notifyOnAsk) {
|
|
19799
|
+
const truncated = command.length > 80 ? command.slice(0, 77) + "..." : command;
|
|
19800
|
+
sendNotification("Claude Warden", `Permission needed: ${truncated}`, config);
|
|
19801
|
+
}
|
|
19716
19802
|
const msg = formatSystemMessage("ask", command, result.details);
|
|
19717
19803
|
const output = {
|
|
19718
19804
|
hookSpecificOutput: {
|