claude-warden 2.5.4 → 2.7.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.
@@ -8,7 +8,7 @@
8
8
  {
9
9
  "name": "warden",
10
10
  "description": "Auto-approves safe commands, blocks dangerous ones, prompts for the rest",
11
- "version": "2.5.4",
11
+ "version": "2.7.0",
12
12
  "author": {
13
13
  "name": "banyudu"
14
14
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "warden",
3
- "version": "2.5.4",
3
+ "version": "2.7.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"
@@ -175,3 +175,25 @@ notifyOnDeny: true
175
175
  # anyArgMatches: ['^(ps|images|logs)$']
176
176
  # decision: allow
177
177
  # description: Read-only docker commands
178
+
179
+ # Skill (slash command) rules — filter Claude Code skill invocations.
180
+ # Skill names use the short form (e.g. "commit", not "/commit").
181
+ # Glob patterns supported for namespace matching (e.g. "example-plugin:*").
182
+ # Built-in defaults auto-allow common safe skills (commit, review, simplify, init).
183
+ # skills:
184
+ # defaultDecision: ask
185
+ # alwaysAllow:
186
+ # - commit
187
+ # - review
188
+ # - simplify
189
+ # - "example-plugin:*"
190
+ # alwaysDeny:
191
+ # - deploy
192
+ # rules:
193
+ # - skill: release
194
+ # default: ask
195
+ # argPatterns:
196
+ # - match:
197
+ # argsMatch: ["--dry-run"]
198
+ # decision: allow
199
+ # description: Dry-run release is safe
package/dist/cli.cjs CHANGED
@@ -18888,6 +18888,51 @@ function pkgRunnerRule(command) {
18888
18888
  ]
18889
18889
  };
18890
18890
  }
18891
+ var DEFAULT_SKILL_RULES = {
18892
+ defaultDecision: "ask",
18893
+ layers: [{
18894
+ alwaysAllow: [
18895
+ // Built-in review/analysis skills
18896
+ "review",
18897
+ "security-review",
18898
+ // Code review plugins
18899
+ "code-review:code-review",
18900
+ "pr-review-toolkit:review-pr",
18901
+ // Slack read-only skills
18902
+ "slack:find-discussions",
18903
+ "slack:summarize-channel",
18904
+ "slack:channel-digest",
18905
+ "slack:standup",
18906
+ "slack:draft-announcement",
18907
+ "slack:slack-messaging",
18908
+ "slack:slack-search",
18909
+ // Search/summarization
18910
+ "promptfolio-summarize",
18911
+ "promptfolio-search-skills",
18912
+ "promptfolio-search-people",
18913
+ // Informational/guidance skills
18914
+ "keybindings-help",
18915
+ "claude-api",
18916
+ "azure-tools:azure-usage",
18917
+ "gcloud-tools:gcloud-usage",
18918
+ "linear-tools:linear-usage",
18919
+ "tavily-tools:tavily-usage",
18920
+ "mongodb-tools:mongodb-usage",
18921
+ "supabase-tools:supabase-usage",
18922
+ "playwright-tools:playwright-testing",
18923
+ // Plugin development guidance (read-only context loading)
18924
+ "plugin-dev:agent-development",
18925
+ "plugin-dev:mcp-integration",
18926
+ "plugin-dev:skill-development",
18927
+ "plugin-dev:plugin-settings",
18928
+ "plugin-dev:command-development",
18929
+ "plugin-dev:plugin-structure",
18930
+ "plugin-dev:hook-development"
18931
+ ],
18932
+ alwaysDeny: [],
18933
+ rules: []
18934
+ }]
18935
+ };
18891
18936
  var DEFAULT_CONFIG = {
18892
18937
  defaultDecision: "ask",
18893
18938
  askOnSubshell: true,
@@ -18895,6 +18940,7 @@ var DEFAULT_CONFIG = {
18895
18940
  notifyOnDeny: true,
18896
18941
  trustedRemotes: [],
18897
18942
  targetPolicies: [],
18943
+ skillRules: DEFAULT_SKILL_RULES,
18898
18944
  layers: [{
18899
18945
  alwaysAllow: [
18900
18946
  // Read-only file operations
@@ -19400,16 +19446,21 @@ var DEFAULT_CONFIG = {
19400
19446
  })),
19401
19447
  // --- Cloud CLIs ---
19402
19448
  { command: "gcloud", default: "ask", argPatterns: [
19403
- { match: { anyArgMatches: ["^(info|version|help|config|components)$"] }, decision: "allow", description: "Config/info" },
19404
- { match: { anyArgMatches: ["^(list|describe|get-iam-policy|get)$"] }, decision: "allow", description: "Read-only ops" },
19449
+ { match: { anyArgMatches: ["^(info|version|help|topic|components|feedback|survey)$"] }, decision: "allow", description: "Info/meta commands" },
19450
+ { match: { anyArgMatches: ["^config$"] }, decision: "allow", description: "Config subcommands" },
19451
+ { match: { anyArgMatches: ["^(list|describe|get|get-iam-policy|browse|tail|read|show|search|lookup|check)$"] }, decision: "allow", description: "Read-only verbs" },
19452
+ { match: { anyArgMatches: ["^(list|get|describe|show|print|read|search|lookup|check)-[a-z][a-z0-9-]*$"] }, decision: "allow", description: "Read-only verb-noun (list-enabled, get-value, print-access-token, etc.)" },
19405
19453
  VERSION_HELP_FLAGS
19406
19454
  ] },
19407
19455
  { command: "az", default: "ask", argPatterns: [
19408
- { match: { anyArgMatches: ["^(list|show|get)$"] }, decision: "allow", description: "Read-only ops" },
19456
+ { match: { anyArgMatches: ["^(list|show|get|search|check)$"] }, decision: "allow", description: "Read-only verbs" },
19457
+ { match: { anyArgMatches: ["^(list|show|get|search|check)-[a-z][a-z0-9-]*$"] }, decision: "allow", description: "Read-only verb-noun" },
19458
+ { match: { anyArgMatches: ["^(version|help|account|feedback)$"] }, decision: "allow", description: "Info/meta commands" },
19409
19459
  VERSION_HELP_FLAGS
19410
19460
  ] },
19411
19461
  { command: "aws", default: "ask", argPatterns: [
19412
- { match: { anyArgMatches: ["^(describe|list|get|sts)$"] }, decision: "allow", description: "Read-only ops" },
19462
+ { match: { anyArgMatches: ["^(describe|list|get|sts|help|search|lookup|check)$"] }, decision: "allow", description: "Read-only verbs" },
19463
+ { match: { anyArgMatches: ["^(describe|list|get|search|lookup|check)-[a-z][a-z0-9-]*$"] }, decision: "allow", description: "Read-only verb-noun" },
19413
19464
  VERSION_HELP_FLAGS
19414
19465
  ] },
19415
19466
  // --- Helm ---
@@ -19520,6 +19571,14 @@ function loadConfig(cwd) {
19520
19571
  ...userLayer ? [userLayer] : [],
19521
19572
  defaultLayer
19522
19573
  ];
19574
+ const defaultSkillLayer = config.skillRules.layers[0];
19575
+ const userSkillLayer = userRaw?.skills ? extractSkillLayer(userRaw.skills) : null;
19576
+ const workspaceSkillLayer = workspaceRaw?.skills ? extractSkillLayer(workspaceRaw.skills) : null;
19577
+ config.skillRules.layers = [
19578
+ ...workspaceSkillLayer ? [workspaceSkillLayer] : [],
19579
+ ...userSkillLayer ? [userSkillLayer] : [],
19580
+ defaultSkillLayer
19581
+ ];
19523
19582
  if (userRaw) mergeNonLayerFields(config, userRaw);
19524
19583
  if (workspaceRaw) mergeNonLayerFields(config, workspaceRaw);
19525
19584
  return config;
@@ -19538,19 +19597,19 @@ function tryLoadFile(filePath) {
19538
19597
  }
19539
19598
  return null;
19540
19599
  }
19541
- function extractLayer(raw) {
19600
+ function extractGenericLayer(raw, nameKey) {
19542
19601
  const rules = Array.isArray(raw.rules) ? raw.rules : [];
19543
19602
  for (const rule of rules) {
19544
19603
  if (rule && typeof rule === "object") {
19545
19604
  if (rule.default && !isValidDecision(rule.default)) {
19546
- warn(`[warden] Warning: invalid rule default "${rule.default}" for "${rule.command}", using "ask"
19605
+ warn(`[warden] Warning: invalid rule default "${rule.default}" for "${rule[nameKey]}", using "ask"
19547
19606
  `);
19548
19607
  rule.default = "ask";
19549
19608
  }
19550
19609
  if (Array.isArray(rule.argPatterns)) {
19551
19610
  for (const pattern of rule.argPatterns) {
19552
19611
  if (pattern?.decision && !isValidDecision(pattern.decision)) {
19553
- warn(`[warden] Warning: invalid pattern decision "${pattern.decision}" for "${rule.command}", using "ask"
19612
+ warn(`[warden] Warning: invalid pattern decision "${pattern.decision}" for "${rule[nameKey]}", using "ask"
19554
19613
  `);
19555
19614
  pattern.decision = "ask";
19556
19615
  }
@@ -19564,6 +19623,12 @@ function extractLayer(raw) {
19564
19623
  rules
19565
19624
  };
19566
19625
  }
19626
+ function extractSkillLayer(raw) {
19627
+ return extractGenericLayer(raw, "skill");
19628
+ }
19629
+ function extractLayer(raw) {
19630
+ return extractGenericLayer(raw, "command");
19631
+ }
19567
19632
  function parseTrustedList(raw) {
19568
19633
  return raw.map((entry) => {
19569
19634
  if (typeof entry === "string") return { name: entry };
@@ -19744,6 +19809,17 @@ function mergeNonLayerFields(config, raw) {
19744
19809
  if (typeof raw.notifyOnDeny === "boolean") {
19745
19810
  config.notifyOnDeny = raw.notifyOnDeny;
19746
19811
  }
19812
+ if (raw.skills && typeof raw.skills === "object") {
19813
+ const skills = raw.skills;
19814
+ if (typeof skills.defaultDecision === "string") {
19815
+ if (isValidDecision(skills.defaultDecision)) {
19816
+ config.skillRules.defaultDecision = skills.defaultDecision;
19817
+ } else {
19818
+ warn(`[warden] Warning: invalid skills.defaultDecision "${skills.defaultDecision}", ignoring
19819
+ `);
19820
+ }
19821
+ }
19822
+ }
19747
19823
  if (raw.trustedContextOverrides && typeof raw.trustedContextOverrides === "object") {
19748
19824
  const overrides = raw.trustedContextOverrides;
19749
19825
  const layer = extractLayer(overrides);
@@ -18892,6 +18892,51 @@ function pkgRunnerRule(command) {
18892
18892
  ]
18893
18893
  };
18894
18894
  }
18895
+ var DEFAULT_SKILL_RULES = {
18896
+ defaultDecision: "ask",
18897
+ layers: [{
18898
+ alwaysAllow: [
18899
+ // Built-in review/analysis skills
18900
+ "review",
18901
+ "security-review",
18902
+ // Code review plugins
18903
+ "code-review:code-review",
18904
+ "pr-review-toolkit:review-pr",
18905
+ // Slack read-only skills
18906
+ "slack:find-discussions",
18907
+ "slack:summarize-channel",
18908
+ "slack:channel-digest",
18909
+ "slack:standup",
18910
+ "slack:draft-announcement",
18911
+ "slack:slack-messaging",
18912
+ "slack:slack-search",
18913
+ // Search/summarization
18914
+ "promptfolio-summarize",
18915
+ "promptfolio-search-skills",
18916
+ "promptfolio-search-people",
18917
+ // Informational/guidance skills
18918
+ "keybindings-help",
18919
+ "claude-api",
18920
+ "azure-tools:azure-usage",
18921
+ "gcloud-tools:gcloud-usage",
18922
+ "linear-tools:linear-usage",
18923
+ "tavily-tools:tavily-usage",
18924
+ "mongodb-tools:mongodb-usage",
18925
+ "supabase-tools:supabase-usage",
18926
+ "playwright-tools:playwright-testing",
18927
+ // Plugin development guidance (read-only context loading)
18928
+ "plugin-dev:agent-development",
18929
+ "plugin-dev:mcp-integration",
18930
+ "plugin-dev:skill-development",
18931
+ "plugin-dev:plugin-settings",
18932
+ "plugin-dev:command-development",
18933
+ "plugin-dev:plugin-structure",
18934
+ "plugin-dev:hook-development"
18935
+ ],
18936
+ alwaysDeny: [],
18937
+ rules: []
18938
+ }]
18939
+ };
18895
18940
  var DEFAULT_CONFIG = {
18896
18941
  defaultDecision: "ask",
18897
18942
  askOnSubshell: true,
@@ -18899,6 +18944,7 @@ var DEFAULT_CONFIG = {
18899
18944
  notifyOnDeny: true,
18900
18945
  trustedRemotes: [],
18901
18946
  targetPolicies: [],
18947
+ skillRules: DEFAULT_SKILL_RULES,
18902
18948
  layers: [{
18903
18949
  alwaysAllow: [
18904
18950
  // Read-only file operations
@@ -19404,16 +19450,21 @@ var DEFAULT_CONFIG = {
19404
19450
  })),
19405
19451
  // --- Cloud CLIs ---
19406
19452
  { command: "gcloud", default: "ask", argPatterns: [
19407
- { match: { anyArgMatches: ["^(info|version|help|config|components)$"] }, decision: "allow", description: "Config/info" },
19408
- { match: { anyArgMatches: ["^(list|describe|get-iam-policy|get)$"] }, decision: "allow", description: "Read-only ops" },
19453
+ { match: { anyArgMatches: ["^(info|version|help|topic|components|feedback|survey)$"] }, decision: "allow", description: "Info/meta commands" },
19454
+ { match: { anyArgMatches: ["^config$"] }, decision: "allow", description: "Config subcommands" },
19455
+ { match: { anyArgMatches: ["^(list|describe|get|get-iam-policy|browse|tail|read|show|search|lookup|check)$"] }, decision: "allow", description: "Read-only verbs" },
19456
+ { match: { anyArgMatches: ["^(list|get|describe|show|print|read|search|lookup|check)-[a-z][a-z0-9-]*$"] }, decision: "allow", description: "Read-only verb-noun (list-enabled, get-value, print-access-token, etc.)" },
19409
19457
  VERSION_HELP_FLAGS
19410
19458
  ] },
19411
19459
  { command: "az", default: "ask", argPatterns: [
19412
- { match: { anyArgMatches: ["^(list|show|get)$"] }, decision: "allow", description: "Read-only ops" },
19460
+ { match: { anyArgMatches: ["^(list|show|get|search|check)$"] }, decision: "allow", description: "Read-only verbs" },
19461
+ { match: { anyArgMatches: ["^(list|show|get|search|check)-[a-z][a-z0-9-]*$"] }, decision: "allow", description: "Read-only verb-noun" },
19462
+ { match: { anyArgMatches: ["^(version|help|account|feedback)$"] }, decision: "allow", description: "Info/meta commands" },
19413
19463
  VERSION_HELP_FLAGS
19414
19464
  ] },
19415
19465
  { command: "aws", default: "ask", argPatterns: [
19416
- { match: { anyArgMatches: ["^(describe|list|get|sts)$"] }, decision: "allow", description: "Read-only ops" },
19466
+ { match: { anyArgMatches: ["^(describe|list|get|sts|help|search|lookup|check)$"] }, decision: "allow", description: "Read-only verbs" },
19467
+ { match: { anyArgMatches: ["^(describe|list|get|search|lookup|check)-[a-z][a-z0-9-]*$"] }, decision: "allow", description: "Read-only verb-noun" },
19417
19468
  VERSION_HELP_FLAGS
19418
19469
  ] },
19419
19470
  // --- Helm ---
@@ -19524,6 +19575,14 @@ function loadConfig(cwd) {
19524
19575
  ...userLayer ? [userLayer] : [],
19525
19576
  defaultLayer
19526
19577
  ];
19578
+ const defaultSkillLayer = config.skillRules.layers[0];
19579
+ const userSkillLayer = userRaw?.skills ? extractSkillLayer(userRaw.skills) : null;
19580
+ const workspaceSkillLayer = workspaceRaw?.skills ? extractSkillLayer(workspaceRaw.skills) : null;
19581
+ config.skillRules.layers = [
19582
+ ...workspaceSkillLayer ? [workspaceSkillLayer] : [],
19583
+ ...userSkillLayer ? [userSkillLayer] : [],
19584
+ defaultSkillLayer
19585
+ ];
19527
19586
  if (userRaw) mergeNonLayerFields(config, userRaw);
19528
19587
  if (workspaceRaw) mergeNonLayerFields(config, workspaceRaw);
19529
19588
  return config;
@@ -19542,19 +19601,19 @@ function tryLoadFile(filePath) {
19542
19601
  }
19543
19602
  return null;
19544
19603
  }
19545
- function extractLayer(raw) {
19604
+ function extractGenericLayer(raw, nameKey) {
19546
19605
  const rules = Array.isArray(raw.rules) ? raw.rules : [];
19547
19606
  for (const rule of rules) {
19548
19607
  if (rule && typeof rule === "object") {
19549
19608
  if (rule.default && !isValidDecision(rule.default)) {
19550
- warn(`[warden] Warning: invalid rule default "${rule.default}" for "${rule.command}", using "ask"
19609
+ warn(`[warden] Warning: invalid rule default "${rule.default}" for "${rule[nameKey]}", using "ask"
19551
19610
  `);
19552
19611
  rule.default = "ask";
19553
19612
  }
19554
19613
  if (Array.isArray(rule.argPatterns)) {
19555
19614
  for (const pattern of rule.argPatterns) {
19556
19615
  if (pattern?.decision && !isValidDecision(pattern.decision)) {
19557
- warn(`[warden] Warning: invalid pattern decision "${pattern.decision}" for "${rule.command}", using "ask"
19616
+ warn(`[warden] Warning: invalid pattern decision "${pattern.decision}" for "${rule[nameKey]}", using "ask"
19558
19617
  `);
19559
19618
  pattern.decision = "ask";
19560
19619
  }
@@ -19568,6 +19627,12 @@ function extractLayer(raw) {
19568
19627
  rules
19569
19628
  };
19570
19629
  }
19630
+ function extractSkillLayer(raw) {
19631
+ return extractGenericLayer(raw, "skill");
19632
+ }
19633
+ function extractLayer(raw) {
19634
+ return extractGenericLayer(raw, "command");
19635
+ }
19571
19636
  function parseTrustedList(raw) {
19572
19637
  return raw.map((entry) => {
19573
19638
  if (typeof entry === "string") return { name: entry };
@@ -19748,6 +19813,17 @@ function mergeNonLayerFields(config, raw) {
19748
19813
  if (typeof raw.notifyOnDeny === "boolean") {
19749
19814
  config.notifyOnDeny = raw.notifyOnDeny;
19750
19815
  }
19816
+ if (raw.skills && typeof raw.skills === "object") {
19817
+ const skills = raw.skills;
19818
+ if (typeof skills.defaultDecision === "string") {
19819
+ if (isValidDecision(skills.defaultDecision)) {
19820
+ config.skillRules.defaultDecision = skills.defaultDecision;
19821
+ } else {
19822
+ warn(`[warden] Warning: invalid skills.defaultDecision "${skills.defaultDecision}", ignoring
19823
+ `);
19824
+ }
19825
+ }
19826
+ }
19751
19827
  if (raw.trustedContextOverrides && typeof raw.trustedContextOverrides === "object") {
19752
19828
  const overrides = raw.trustedContextOverrides;
19753
19829
  const layer = extractLayer(overrides);
package/dist/copilot.cjs CHANGED
@@ -18888,6 +18888,51 @@ function pkgRunnerRule(command) {
18888
18888
  ]
18889
18889
  };
18890
18890
  }
18891
+ var DEFAULT_SKILL_RULES = {
18892
+ defaultDecision: "ask",
18893
+ layers: [{
18894
+ alwaysAllow: [
18895
+ // Built-in review/analysis skills
18896
+ "review",
18897
+ "security-review",
18898
+ // Code review plugins
18899
+ "code-review:code-review",
18900
+ "pr-review-toolkit:review-pr",
18901
+ // Slack read-only skills
18902
+ "slack:find-discussions",
18903
+ "slack:summarize-channel",
18904
+ "slack:channel-digest",
18905
+ "slack:standup",
18906
+ "slack:draft-announcement",
18907
+ "slack:slack-messaging",
18908
+ "slack:slack-search",
18909
+ // Search/summarization
18910
+ "promptfolio-summarize",
18911
+ "promptfolio-search-skills",
18912
+ "promptfolio-search-people",
18913
+ // Informational/guidance skills
18914
+ "keybindings-help",
18915
+ "claude-api",
18916
+ "azure-tools:azure-usage",
18917
+ "gcloud-tools:gcloud-usage",
18918
+ "linear-tools:linear-usage",
18919
+ "tavily-tools:tavily-usage",
18920
+ "mongodb-tools:mongodb-usage",
18921
+ "supabase-tools:supabase-usage",
18922
+ "playwright-tools:playwright-testing",
18923
+ // Plugin development guidance (read-only context loading)
18924
+ "plugin-dev:agent-development",
18925
+ "plugin-dev:mcp-integration",
18926
+ "plugin-dev:skill-development",
18927
+ "plugin-dev:plugin-settings",
18928
+ "plugin-dev:command-development",
18929
+ "plugin-dev:plugin-structure",
18930
+ "plugin-dev:hook-development"
18931
+ ],
18932
+ alwaysDeny: [],
18933
+ rules: []
18934
+ }]
18935
+ };
18891
18936
  var DEFAULT_CONFIG = {
18892
18937
  defaultDecision: "ask",
18893
18938
  askOnSubshell: true,
@@ -18895,6 +18940,7 @@ var DEFAULT_CONFIG = {
18895
18940
  notifyOnDeny: true,
18896
18941
  trustedRemotes: [],
18897
18942
  targetPolicies: [],
18943
+ skillRules: DEFAULT_SKILL_RULES,
18898
18944
  layers: [{
18899
18945
  alwaysAllow: [
18900
18946
  // Read-only file operations
@@ -19400,16 +19446,21 @@ var DEFAULT_CONFIG = {
19400
19446
  })),
19401
19447
  // --- Cloud CLIs ---
19402
19448
  { command: "gcloud", default: "ask", argPatterns: [
19403
- { match: { anyArgMatches: ["^(info|version|help|config|components)$"] }, decision: "allow", description: "Config/info" },
19404
- { match: { anyArgMatches: ["^(list|describe|get-iam-policy|get)$"] }, decision: "allow", description: "Read-only ops" },
19449
+ { match: { anyArgMatches: ["^(info|version|help|topic|components|feedback|survey)$"] }, decision: "allow", description: "Info/meta commands" },
19450
+ { match: { anyArgMatches: ["^config$"] }, decision: "allow", description: "Config subcommands" },
19451
+ { match: { anyArgMatches: ["^(list|describe|get|get-iam-policy|browse|tail|read|show|search|lookup|check)$"] }, decision: "allow", description: "Read-only verbs" },
19452
+ { match: { anyArgMatches: ["^(list|get|describe|show|print|read|search|lookup|check)-[a-z][a-z0-9-]*$"] }, decision: "allow", description: "Read-only verb-noun (list-enabled, get-value, print-access-token, etc.)" },
19405
19453
  VERSION_HELP_FLAGS
19406
19454
  ] },
19407
19455
  { command: "az", default: "ask", argPatterns: [
19408
- { match: { anyArgMatches: ["^(list|show|get)$"] }, decision: "allow", description: "Read-only ops" },
19456
+ { match: { anyArgMatches: ["^(list|show|get|search|check)$"] }, decision: "allow", description: "Read-only verbs" },
19457
+ { match: { anyArgMatches: ["^(list|show|get|search|check)-[a-z][a-z0-9-]*$"] }, decision: "allow", description: "Read-only verb-noun" },
19458
+ { match: { anyArgMatches: ["^(version|help|account|feedback)$"] }, decision: "allow", description: "Info/meta commands" },
19409
19459
  VERSION_HELP_FLAGS
19410
19460
  ] },
19411
19461
  { command: "aws", default: "ask", argPatterns: [
19412
- { match: { anyArgMatches: ["^(describe|list|get|sts)$"] }, decision: "allow", description: "Read-only ops" },
19462
+ { match: { anyArgMatches: ["^(describe|list|get|sts|help|search|lookup|check)$"] }, decision: "allow", description: "Read-only verbs" },
19463
+ { match: { anyArgMatches: ["^(describe|list|get|search|lookup|check)-[a-z][a-z0-9-]*$"] }, decision: "allow", description: "Read-only verb-noun" },
19413
19464
  VERSION_HELP_FLAGS
19414
19465
  ] },
19415
19466
  // --- Helm ---
@@ -19517,6 +19568,14 @@ function loadConfig(cwd) {
19517
19568
  ...userLayer ? [userLayer] : [],
19518
19569
  defaultLayer
19519
19570
  ];
19571
+ const defaultSkillLayer = config.skillRules.layers[0];
19572
+ const userSkillLayer = userRaw?.skills ? extractSkillLayer(userRaw.skills) : null;
19573
+ const workspaceSkillLayer = workspaceRaw?.skills ? extractSkillLayer(workspaceRaw.skills) : null;
19574
+ config.skillRules.layers = [
19575
+ ...workspaceSkillLayer ? [workspaceSkillLayer] : [],
19576
+ ...userSkillLayer ? [userSkillLayer] : [],
19577
+ defaultSkillLayer
19578
+ ];
19520
19579
  if (userRaw) mergeNonLayerFields(config, userRaw);
19521
19580
  if (workspaceRaw) mergeNonLayerFields(config, workspaceRaw);
19522
19581
  return config;
@@ -19535,19 +19594,19 @@ function tryLoadFile(filePath) {
19535
19594
  }
19536
19595
  return null;
19537
19596
  }
19538
- function extractLayer(raw) {
19597
+ function extractGenericLayer(raw, nameKey) {
19539
19598
  const rules = Array.isArray(raw.rules) ? raw.rules : [];
19540
19599
  for (const rule of rules) {
19541
19600
  if (rule && typeof rule === "object") {
19542
19601
  if (rule.default && !isValidDecision(rule.default)) {
19543
- warn(`[warden] Warning: invalid rule default "${rule.default}" for "${rule.command}", using "ask"
19602
+ warn(`[warden] Warning: invalid rule default "${rule.default}" for "${rule[nameKey]}", using "ask"
19544
19603
  `);
19545
19604
  rule.default = "ask";
19546
19605
  }
19547
19606
  if (Array.isArray(rule.argPatterns)) {
19548
19607
  for (const pattern of rule.argPatterns) {
19549
19608
  if (pattern?.decision && !isValidDecision(pattern.decision)) {
19550
- warn(`[warden] Warning: invalid pattern decision "${pattern.decision}" for "${rule.command}", using "ask"
19609
+ warn(`[warden] Warning: invalid pattern decision "${pattern.decision}" for "${rule[nameKey]}", using "ask"
19551
19610
  `);
19552
19611
  pattern.decision = "ask";
19553
19612
  }
@@ -19561,6 +19620,12 @@ function extractLayer(raw) {
19561
19620
  rules
19562
19621
  };
19563
19622
  }
19623
+ function extractSkillLayer(raw) {
19624
+ return extractGenericLayer(raw, "skill");
19625
+ }
19626
+ function extractLayer(raw) {
19627
+ return extractGenericLayer(raw, "command");
19628
+ }
19564
19629
  function parseTrustedList(raw) {
19565
19630
  return raw.map((entry) => {
19566
19631
  if (typeof entry === "string") return { name: entry };
@@ -19741,6 +19806,17 @@ function mergeNonLayerFields(config, raw) {
19741
19806
  if (typeof raw.notifyOnDeny === "boolean") {
19742
19807
  config.notifyOnDeny = raw.notifyOnDeny;
19743
19808
  }
19809
+ if (raw.skills && typeof raw.skills === "object") {
19810
+ const skills = raw.skills;
19811
+ if (typeof skills.defaultDecision === "string") {
19812
+ if (isValidDecision(skills.defaultDecision)) {
19813
+ config.skillRules.defaultDecision = skills.defaultDecision;
19814
+ } else {
19815
+ warn(`[warden] Warning: invalid skills.defaultDecision "${skills.defaultDecision}", ignoring
19816
+ `);
19817
+ }
19818
+ }
19819
+ }
19744
19820
  if (raw.trustedContextOverrides && typeof raw.trustedContextOverrides === "object") {
19745
19821
  const overrides = raw.trustedContextOverrides;
19746
19822
  const layer = extractLayer(overrides);
package/dist/index.cjs CHANGED
@@ -18888,6 +18888,51 @@ function pkgRunnerRule(command) {
18888
18888
  ]
18889
18889
  };
18890
18890
  }
18891
+ var DEFAULT_SKILL_RULES = {
18892
+ defaultDecision: "ask",
18893
+ layers: [{
18894
+ alwaysAllow: [
18895
+ // Built-in review/analysis skills
18896
+ "review",
18897
+ "security-review",
18898
+ // Code review plugins
18899
+ "code-review:code-review",
18900
+ "pr-review-toolkit:review-pr",
18901
+ // Slack read-only skills
18902
+ "slack:find-discussions",
18903
+ "slack:summarize-channel",
18904
+ "slack:channel-digest",
18905
+ "slack:standup",
18906
+ "slack:draft-announcement",
18907
+ "slack:slack-messaging",
18908
+ "slack:slack-search",
18909
+ // Search/summarization
18910
+ "promptfolio-summarize",
18911
+ "promptfolio-search-skills",
18912
+ "promptfolio-search-people",
18913
+ // Informational/guidance skills
18914
+ "keybindings-help",
18915
+ "claude-api",
18916
+ "azure-tools:azure-usage",
18917
+ "gcloud-tools:gcloud-usage",
18918
+ "linear-tools:linear-usage",
18919
+ "tavily-tools:tavily-usage",
18920
+ "mongodb-tools:mongodb-usage",
18921
+ "supabase-tools:supabase-usage",
18922
+ "playwright-tools:playwright-testing",
18923
+ // Plugin development guidance (read-only context loading)
18924
+ "plugin-dev:agent-development",
18925
+ "plugin-dev:mcp-integration",
18926
+ "plugin-dev:skill-development",
18927
+ "plugin-dev:plugin-settings",
18928
+ "plugin-dev:command-development",
18929
+ "plugin-dev:plugin-structure",
18930
+ "plugin-dev:hook-development"
18931
+ ],
18932
+ alwaysDeny: [],
18933
+ rules: []
18934
+ }]
18935
+ };
18891
18936
  var DEFAULT_CONFIG = {
18892
18937
  defaultDecision: "ask",
18893
18938
  askOnSubshell: true,
@@ -18895,6 +18940,7 @@ var DEFAULT_CONFIG = {
18895
18940
  notifyOnDeny: true,
18896
18941
  trustedRemotes: [],
18897
18942
  targetPolicies: [],
18943
+ skillRules: DEFAULT_SKILL_RULES,
18898
18944
  layers: [{
18899
18945
  alwaysAllow: [
18900
18946
  // Read-only file operations
@@ -19400,16 +19446,21 @@ var DEFAULT_CONFIG = {
19400
19446
  })),
19401
19447
  // --- Cloud CLIs ---
19402
19448
  { command: "gcloud", default: "ask", argPatterns: [
19403
- { match: { anyArgMatches: ["^(info|version|help|config|components)$"] }, decision: "allow", description: "Config/info" },
19404
- { match: { anyArgMatches: ["^(list|describe|get-iam-policy|get)$"] }, decision: "allow", description: "Read-only ops" },
19449
+ { match: { anyArgMatches: ["^(info|version|help|topic|components|feedback|survey)$"] }, decision: "allow", description: "Info/meta commands" },
19450
+ { match: { anyArgMatches: ["^config$"] }, decision: "allow", description: "Config subcommands" },
19451
+ { match: { anyArgMatches: ["^(list|describe|get|get-iam-policy|browse|tail|read|show|search|lookup|check)$"] }, decision: "allow", description: "Read-only verbs" },
19452
+ { match: { anyArgMatches: ["^(list|get|describe|show|print|read|search|lookup|check)-[a-z][a-z0-9-]*$"] }, decision: "allow", description: "Read-only verb-noun (list-enabled, get-value, print-access-token, etc.)" },
19405
19453
  VERSION_HELP_FLAGS
19406
19454
  ] },
19407
19455
  { command: "az", default: "ask", argPatterns: [
19408
- { match: { anyArgMatches: ["^(list|show|get)$"] }, decision: "allow", description: "Read-only ops" },
19456
+ { match: { anyArgMatches: ["^(list|show|get|search|check)$"] }, decision: "allow", description: "Read-only verbs" },
19457
+ { match: { anyArgMatches: ["^(list|show|get|search|check)-[a-z][a-z0-9-]*$"] }, decision: "allow", description: "Read-only verb-noun" },
19458
+ { match: { anyArgMatches: ["^(version|help|account|feedback)$"] }, decision: "allow", description: "Info/meta commands" },
19409
19459
  VERSION_HELP_FLAGS
19410
19460
  ] },
19411
19461
  { command: "aws", default: "ask", argPatterns: [
19412
- { match: { anyArgMatches: ["^(describe|list|get|sts)$"] }, decision: "allow", description: "Read-only ops" },
19462
+ { match: { anyArgMatches: ["^(describe|list|get|sts|help|search|lookup|check)$"] }, decision: "allow", description: "Read-only verbs" },
19463
+ { match: { anyArgMatches: ["^(describe|list|get|search|lookup|check)-[a-z][a-z0-9-]*$"] }, decision: "allow", description: "Read-only verb-noun" },
19413
19464
  VERSION_HELP_FLAGS
19414
19465
  ] },
19415
19466
  // --- Helm ---
@@ -19517,6 +19568,14 @@ function loadConfig(cwd) {
19517
19568
  ...userLayer ? [userLayer] : [],
19518
19569
  defaultLayer
19519
19570
  ];
19571
+ const defaultSkillLayer = config.skillRules.layers[0];
19572
+ const userSkillLayer = userRaw?.skills ? extractSkillLayer(userRaw.skills) : null;
19573
+ const workspaceSkillLayer = workspaceRaw?.skills ? extractSkillLayer(workspaceRaw.skills) : null;
19574
+ config.skillRules.layers = [
19575
+ ...workspaceSkillLayer ? [workspaceSkillLayer] : [],
19576
+ ...userSkillLayer ? [userSkillLayer] : [],
19577
+ defaultSkillLayer
19578
+ ];
19520
19579
  if (userRaw) mergeNonLayerFields(config, userRaw);
19521
19580
  if (workspaceRaw) mergeNonLayerFields(config, workspaceRaw);
19522
19581
  return config;
@@ -19535,19 +19594,19 @@ function tryLoadFile(filePath) {
19535
19594
  }
19536
19595
  return null;
19537
19596
  }
19538
- function extractLayer(raw) {
19597
+ function extractGenericLayer(raw, nameKey) {
19539
19598
  const rules = Array.isArray(raw.rules) ? raw.rules : [];
19540
19599
  for (const rule of rules) {
19541
19600
  if (rule && typeof rule === "object") {
19542
19601
  if (rule.default && !isValidDecision(rule.default)) {
19543
- warn(`[warden] Warning: invalid rule default "${rule.default}" for "${rule.command}", using "ask"
19602
+ warn(`[warden] Warning: invalid rule default "${rule.default}" for "${rule[nameKey]}", using "ask"
19544
19603
  `);
19545
19604
  rule.default = "ask";
19546
19605
  }
19547
19606
  if (Array.isArray(rule.argPatterns)) {
19548
19607
  for (const pattern of rule.argPatterns) {
19549
19608
  if (pattern?.decision && !isValidDecision(pattern.decision)) {
19550
- warn(`[warden] Warning: invalid pattern decision "${pattern.decision}" for "${rule.command}", using "ask"
19609
+ warn(`[warden] Warning: invalid pattern decision "${pattern.decision}" for "${rule[nameKey]}", using "ask"
19551
19610
  `);
19552
19611
  pattern.decision = "ask";
19553
19612
  }
@@ -19561,6 +19620,12 @@ function extractLayer(raw) {
19561
19620
  rules
19562
19621
  };
19563
19622
  }
19623
+ function extractSkillLayer(raw) {
19624
+ return extractGenericLayer(raw, "skill");
19625
+ }
19626
+ function extractLayer(raw) {
19627
+ return extractGenericLayer(raw, "command");
19628
+ }
19564
19629
  function parseTrustedList(raw) {
19565
19630
  return raw.map((entry) => {
19566
19631
  if (typeof entry === "string") return { name: entry };
@@ -19741,6 +19806,17 @@ function mergeNonLayerFields(config, raw) {
19741
19806
  if (typeof raw.notifyOnDeny === "boolean") {
19742
19807
  config.notifyOnDeny = raw.notifyOnDeny;
19743
19808
  }
19809
+ if (raw.skills && typeof raw.skills === "object") {
19810
+ const skills = raw.skills;
19811
+ if (typeof skills.defaultDecision === "string") {
19812
+ if (isValidDecision(skills.defaultDecision)) {
19813
+ config.skillRules.defaultDecision = skills.defaultDecision;
19814
+ } else {
19815
+ warn(`[warden] Warning: invalid skills.defaultDecision "${skills.defaultDecision}", ignoring
19816
+ `);
19817
+ }
19818
+ }
19819
+ }
19744
19820
  if (raw.trustedContextOverrides && typeof raw.trustedContextOverrides === "object") {
19745
19821
  const overrides = raw.trustedContextOverrides;
19746
19822
  const layer = extractLayer(overrides);
@@ -20594,6 +20670,110 @@ function wardenEvalWithConfig(command, config, cwd) {
20594
20670
  return evaluate(parsed, config, cwd);
20595
20671
  }
20596
20672
 
20673
+ // src/skill-evaluator.ts
20674
+ function skillMatchesName(skillName, pattern) {
20675
+ return globToRegex(pattern).test(skillName);
20676
+ }
20677
+ function evaluateSkill(skillName, args2, config) {
20678
+ const detail = evaluateSkillDetail(skillName, args2, config);
20679
+ return {
20680
+ decision: detail.decision,
20681
+ reason: detail.reason,
20682
+ details: [detail]
20683
+ };
20684
+ }
20685
+ function evaluateSkillDetail(skillName, args2, config) {
20686
+ const { skillRules } = config;
20687
+ for (const layer of skillRules.layers) {
20688
+ if (layer.alwaysDeny.some((p) => skillMatchesName(skillName, p))) {
20689
+ return {
20690
+ command: skillName,
20691
+ args: args2 ? [args2] : [],
20692
+ decision: "deny",
20693
+ reason: `Skill "${skillName}" is blocked`,
20694
+ matchedRule: "alwaysDeny"
20695
+ };
20696
+ }
20697
+ if (layer.alwaysAllow.some((p) => skillMatchesName(skillName, p))) {
20698
+ return {
20699
+ command: skillName,
20700
+ args: args2 ? [args2] : [],
20701
+ decision: "allow",
20702
+ reason: `Skill "${skillName}" is safe`,
20703
+ matchedRule: "alwaysAllow"
20704
+ };
20705
+ }
20706
+ }
20707
+ const mergedRule = collectMergedSkillRule(skillName, skillRules.layers.map((l) => l.rules));
20708
+ if (mergedRule) {
20709
+ return evaluateSkillRule(skillName, args2, mergedRule);
20710
+ }
20711
+ return {
20712
+ command: skillName,
20713
+ args: args2 ? [args2] : [],
20714
+ decision: skillRules.defaultDecision,
20715
+ reason: `No rule for skill "${skillName}"`,
20716
+ matchedRule: "default"
20717
+ };
20718
+ }
20719
+ function collectMergedSkillRule(skillName, layerRules) {
20720
+ const matching = [];
20721
+ for (const rules of layerRules) {
20722
+ const rule = rules.find((r) => skillMatchesName(skillName, r.skill));
20723
+ if (rule) {
20724
+ matching.push(rule);
20725
+ if (rule.override) break;
20726
+ }
20727
+ }
20728
+ if (matching.length === 0) return null;
20729
+ if (matching.length === 1) return matching[0];
20730
+ const mergedPatterns = [];
20731
+ for (const rule of matching) {
20732
+ if (rule.argPatterns) {
20733
+ mergedPatterns.push(...rule.argPatterns);
20734
+ }
20735
+ }
20736
+ return {
20737
+ skill: matching[0].skill,
20738
+ default: matching[0].default,
20739
+ argPatterns: mergedPatterns
20740
+ };
20741
+ }
20742
+ function evaluateSkillRule(skillName, args2, rule) {
20743
+ const argsArray = args2 ? [args2] : [];
20744
+ const argsJoined = args2 || "";
20745
+ for (const pattern of rule.argPatterns || []) {
20746
+ const m = pattern.match;
20747
+ let matched = true;
20748
+ if (m.noArgs !== void 0) {
20749
+ matched = matched && m.noArgs === !args2;
20750
+ }
20751
+ if (m.argsMatch && matched) {
20752
+ matched = m.argsMatch.some((re) => safeRegexTest(re, argsJoined));
20753
+ }
20754
+ if (m.anyArgMatches && matched) {
20755
+ matched = argsArray.some((arg) => m.anyArgMatches.some((re) => safeRegexTest(re, arg)));
20756
+ }
20757
+ if (m.not) matched = !matched;
20758
+ if (matched) {
20759
+ return {
20760
+ command: skillName,
20761
+ args: argsArray,
20762
+ decision: pattern.decision,
20763
+ reason: pattern.reason || pattern.description || `Matched pattern for skill "${skillName}"`,
20764
+ matchedRule: `${skillName}:argPattern`
20765
+ };
20766
+ }
20767
+ }
20768
+ return {
20769
+ command: skillName,
20770
+ args: argsArray,
20771
+ decision: rule.default,
20772
+ reason: `Default for skill "${skillName}"`,
20773
+ matchedRule: `${skillName}:default`
20774
+ };
20775
+ }
20776
+
20597
20777
  // src/suggest.ts
20598
20778
  function generateAllowSnippet(details) {
20599
20779
  const lines = [];
@@ -20794,20 +20974,35 @@ function deactivateYolo(sessionId) {
20794
20974
 
20795
20975
  // src/index.ts
20796
20976
  var MAX_STDIN_SIZE = 1024 * 1024;
20977
+ function emitDecision(decision, reason, stderrMessage) {
20978
+ const output = {
20979
+ hookSpecificOutput: {
20980
+ hookEventName: "PreToolUse",
20981
+ permissionDecision: decision,
20982
+ permissionDecisionReason: reason
20983
+ }
20984
+ };
20985
+ process.stdout.write(JSON.stringify(output));
20986
+ if (decision === "deny") {
20987
+ process.stderr.write(`${stderrMessage ?? reason}
20988
+ `);
20989
+ process.exit(2);
20990
+ }
20991
+ process.exit(0);
20992
+ }
20993
+ function handleYoloMode(sessionId, result) {
20994
+ const yoloState = getYoloState(sessionId);
20995
+ if (!yoloState) return;
20996
+ if (result.decision === "deny" && !yoloState.bypassDeny) return;
20997
+ const expiryInfo = yoloState.expiresAt ? `expires ${new Date(yoloState.expiresAt).toLocaleTimeString()}` : "full session";
20998
+ emitDecision("allow", `[warden] YOLO mode active (${expiryInfo})`);
20999
+ }
20797
21000
  async function main() {
20798
21001
  let raw = "";
20799
21002
  for await (const chunk of process.stdin) {
20800
21003
  raw += chunk;
20801
21004
  if (raw.length > MAX_STDIN_SIZE) {
20802
- const output2 = {
20803
- hookSpecificOutput: {
20804
- hookEventName: "PreToolUse",
20805
- permissionDecision: "ask",
20806
- permissionDecisionReason: "[warden] Input exceeds size limit"
20807
- }
20808
- };
20809
- process.stdout.write(JSON.stringify(output2));
20810
- process.exit(0);
21005
+ emitDecision("ask", "[warden] Input exceeds size limit");
20811
21006
  }
20812
21007
  }
20813
21008
  let input;
@@ -20816,7 +21011,7 @@ async function main() {
20816
21011
  } catch {
20817
21012
  process.exit(0);
20818
21013
  }
20819
- if (input.tool_name !== "Bash") {
21014
+ if (input.tool_name !== "Bash" && input.tool_name !== "Skill") {
20820
21015
  process.exit(0);
20821
21016
  }
20822
21017
  if (input.permission_mode === "bypassPermissions") {
@@ -20825,100 +21020,63 @@ async function main() {
20825
21020
  if (process.env.WARDEN_YOLO === "true" || process.env.WARDEN_YOLO === "1") {
20826
21021
  process.exit(0);
20827
21022
  }
21023
+ if (input.tool_name === "Skill") {
21024
+ const skillName = input.tool_input?.skill;
21025
+ if (!skillName || typeof skillName !== "string") process.exit(0);
21026
+ const args2 = typeof input.tool_input?.args === "string" ? input.tool_input.args : void 0;
21027
+ const config2 = loadConfig(input.cwd);
21028
+ const result2 = evaluateSkill(skillName, args2, config2);
21029
+ handleYoloMode(input.session_id, result2);
21030
+ emitResult(result2, `skill:${skillName}`, config2);
21031
+ }
20828
21032
  const command = input.tool_input?.command;
20829
21033
  if (!command || typeof command !== "string") {
20830
21034
  process.exit(0);
20831
21035
  }
20832
21036
  const yoloCmd = parseYoloCommand(command);
20833
21037
  if (yoloCmd) {
20834
- let msg2;
21038
+ let msg;
20835
21039
  if (yoloCmd.action === "activate") {
20836
21040
  const state = activateYolo(input.session_id, yoloCmd.durationMinutes);
20837
21041
  const expiryInfo = state.expiresAt ? `expires at ${new Date(state.expiresAt).toLocaleTimeString()}` : "full session, no expiry";
20838
- msg2 = `[warden] YOLO mode activated (${expiryInfo}). Always-deny commands are still blocked. Use \`echo __WARDEN_YOLO_DEACTIVATE__\` to turn off.`;
21042
+ msg = `[warden] YOLO mode activated (${expiryInfo}). Always-deny commands are still blocked. Use \`echo __WARDEN_YOLO_DEACTIVATE__\` to turn off.`;
20839
21043
  } else if (yoloCmd.action === "deactivate") {
20840
21044
  deactivateYolo(input.session_id);
20841
- msg2 = "[warden] YOLO mode deactivated. Normal rule evaluation resumed.";
21045
+ msg = "[warden] YOLO mode deactivated. Normal rule evaluation resumed.";
20842
21046
  } else {
20843
21047
  const state = getYoloState(input.session_id);
20844
21048
  if (state) {
20845
21049
  const expiryInfo = state.expiresAt ? `expires at ${new Date(state.expiresAt).toLocaleTimeString()}` : "full session";
20846
- msg2 = `[warden] YOLO mode is active (${expiryInfo})`;
21050
+ msg = `[warden] YOLO mode is active (${expiryInfo})`;
20847
21051
  } else {
20848
- msg2 = "[warden] YOLO mode is not active";
21052
+ msg = "[warden] YOLO mode is not active";
20849
21053
  }
20850
21054
  }
20851
- const output2 = {
20852
- hookSpecificOutput: {
20853
- hookEventName: "PreToolUse",
20854
- permissionDecision: "allow",
20855
- permissionDecisionReason: msg2
20856
- }
20857
- };
20858
- process.stdout.write(JSON.stringify(output2));
20859
- process.exit(0);
21055
+ emitDecision("allow", msg);
20860
21056
  }
20861
21057
  const config = loadConfig(input.cwd);
20862
21058
  const result = wardenEvalWithConfig(command, config, input.cwd);
20863
- const yoloState = getYoloState(input.session_id);
20864
- if (yoloState) {
20865
- if (result.decision === "deny" && !yoloState.bypassDeny) {
20866
- } else {
20867
- const expiryInfo = yoloState.expiresAt ? `expires ${new Date(yoloState.expiresAt).toLocaleTimeString()}` : "full session";
20868
- const output2 = {
20869
- hookSpecificOutput: {
20870
- hookEventName: "PreToolUse",
20871
- permissionDecision: "allow",
20872
- permissionDecisionReason: `[warden] YOLO mode active (${expiryInfo})`
20873
- }
20874
- };
20875
- process.stdout.write(JSON.stringify(output2));
20876
- process.exit(0);
20877
- }
20878
- }
21059
+ handleYoloMode(input.session_id, result);
21060
+ emitResult(result, command, config);
21061
+ }
21062
+ function emitResult(result, label, config) {
20879
21063
  if (result.decision === "allow") {
20880
- const output2 = {
20881
- hookSpecificOutput: {
20882
- hookEventName: "PreToolUse",
20883
- permissionDecision: "allow",
20884
- permissionDecisionReason: `[warden] ${result.reason}`
20885
- }
20886
- };
20887
- process.stdout.write(JSON.stringify(output2));
20888
- process.exit(0);
21064
+ emitDecision("allow", `[warden] ${result.reason}`);
20889
21065
  }
20890
21066
  if (result.decision === "deny") {
20891
21067
  if (config.notifyOnDeny) {
20892
- const truncated = command.length > 80 ? command.slice(0, 77) + "..." : command;
21068
+ const truncated = label.length > 80 ? label.slice(0, 77) + "..." : label;
20893
21069
  sendNotification("Claude Warden", `Blocked: ${truncated}`, config);
20894
21070
  }
20895
- const msg2 = formatSystemMessage("deny", command, result.details);
20896
- const output2 = {
20897
- hookSpecificOutput: {
20898
- hookEventName: "PreToolUse",
20899
- permissionDecision: "deny",
20900
- permissionDecisionReason: msg2
20901
- }
20902
- };
20903
- process.stdout.write(JSON.stringify(output2));
20904
- process.stderr.write(`[warden] Blocked: ${result.reason}
20905
- `);
20906
- process.exit(2);
21071
+ const msg2 = formatSystemMessage("deny", label, result.details);
21072
+ emitDecision("deny", msg2, `[warden] Blocked: ${result.reason}`);
20907
21073
  }
20908
21074
  if (config.notifyOnAsk) {
20909
- const truncated = command.length > 80 ? command.slice(0, 77) + "..." : command;
21075
+ const truncated = label.length > 80 ? label.slice(0, 77) + "..." : label;
20910
21076
  sendNotification("Claude Warden", `Permission needed: ${truncated}`, config);
20911
21077
  }
20912
- const msg = formatSystemMessage("ask", command, result.details);
20913
- const output = {
20914
- hookSpecificOutput: {
20915
- hookEventName: "PreToolUse",
20916
- permissionDecision: "ask",
20917
- permissionDecisionReason: msg
20918
- }
20919
- };
20920
- process.stdout.write(JSON.stringify(output));
20921
- process.exit(0);
21078
+ const msg = formatSystemMessage("ask", label, result.details);
21079
+ emitDecision("ask", msg);
20922
21080
  }
20923
21081
  main().catch(() => process.exit(0));
20924
21082
  /*! Bundled license information:
package/hooks/hooks.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
- "description": "Smart command safety filter — evaluates Bash commands against configurable safety rules",
2
+ "description": "Smart command safety filter — evaluates Bash commands and Skill invocations against configurable safety rules",
3
3
  "hooks": {
4
4
  "PreToolUse": [
5
5
  {
@@ -11,6 +11,16 @@
11
11
  "timeout": 5
12
12
  }
13
13
  ]
14
+ },
15
+ {
16
+ "matcher": "Skill",
17
+ "hooks": [
18
+ {
19
+ "type": "command",
20
+ "command": "${CLAUDE_PLUGIN_ROOT}/dist/index.cjs",
21
+ "timeout": 5
22
+ }
23
+ ]
14
24
  }
15
25
  ]
16
26
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-warden",
3
- "version": "2.5.4",
3
+ "version": "2.7.0",
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",