claude-warden 2.6.0 → 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.6.0",
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.6.0",
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
@@ -19525,6 +19571,14 @@ function loadConfig(cwd) {
19525
19571
  ...userLayer ? [userLayer] : [],
19526
19572
  defaultLayer
19527
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
+ ];
19528
19582
  if (userRaw) mergeNonLayerFields(config, userRaw);
19529
19583
  if (workspaceRaw) mergeNonLayerFields(config, workspaceRaw);
19530
19584
  return config;
@@ -19543,19 +19597,19 @@ function tryLoadFile(filePath) {
19543
19597
  }
19544
19598
  return null;
19545
19599
  }
19546
- function extractLayer(raw) {
19600
+ function extractGenericLayer(raw, nameKey) {
19547
19601
  const rules = Array.isArray(raw.rules) ? raw.rules : [];
19548
19602
  for (const rule of rules) {
19549
19603
  if (rule && typeof rule === "object") {
19550
19604
  if (rule.default && !isValidDecision(rule.default)) {
19551
- 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"
19552
19606
  `);
19553
19607
  rule.default = "ask";
19554
19608
  }
19555
19609
  if (Array.isArray(rule.argPatterns)) {
19556
19610
  for (const pattern of rule.argPatterns) {
19557
19611
  if (pattern?.decision && !isValidDecision(pattern.decision)) {
19558
- 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"
19559
19613
  `);
19560
19614
  pattern.decision = "ask";
19561
19615
  }
@@ -19569,6 +19623,12 @@ function extractLayer(raw) {
19569
19623
  rules
19570
19624
  };
19571
19625
  }
19626
+ function extractSkillLayer(raw) {
19627
+ return extractGenericLayer(raw, "skill");
19628
+ }
19629
+ function extractLayer(raw) {
19630
+ return extractGenericLayer(raw, "command");
19631
+ }
19572
19632
  function parseTrustedList(raw) {
19573
19633
  return raw.map((entry) => {
19574
19634
  if (typeof entry === "string") return { name: entry };
@@ -19749,6 +19809,17 @@ function mergeNonLayerFields(config, raw) {
19749
19809
  if (typeof raw.notifyOnDeny === "boolean") {
19750
19810
  config.notifyOnDeny = raw.notifyOnDeny;
19751
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
+ }
19752
19823
  if (raw.trustedContextOverrides && typeof raw.trustedContextOverrides === "object") {
19753
19824
  const overrides = raw.trustedContextOverrides;
19754
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
@@ -19529,6 +19575,14 @@ function loadConfig(cwd) {
19529
19575
  ...userLayer ? [userLayer] : [],
19530
19576
  defaultLayer
19531
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
+ ];
19532
19586
  if (userRaw) mergeNonLayerFields(config, userRaw);
19533
19587
  if (workspaceRaw) mergeNonLayerFields(config, workspaceRaw);
19534
19588
  return config;
@@ -19547,19 +19601,19 @@ function tryLoadFile(filePath) {
19547
19601
  }
19548
19602
  return null;
19549
19603
  }
19550
- function extractLayer(raw) {
19604
+ function extractGenericLayer(raw, nameKey) {
19551
19605
  const rules = Array.isArray(raw.rules) ? raw.rules : [];
19552
19606
  for (const rule of rules) {
19553
19607
  if (rule && typeof rule === "object") {
19554
19608
  if (rule.default && !isValidDecision(rule.default)) {
19555
- 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"
19556
19610
  `);
19557
19611
  rule.default = "ask";
19558
19612
  }
19559
19613
  if (Array.isArray(rule.argPatterns)) {
19560
19614
  for (const pattern of rule.argPatterns) {
19561
19615
  if (pattern?.decision && !isValidDecision(pattern.decision)) {
19562
- 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"
19563
19617
  `);
19564
19618
  pattern.decision = "ask";
19565
19619
  }
@@ -19573,6 +19627,12 @@ function extractLayer(raw) {
19573
19627
  rules
19574
19628
  };
19575
19629
  }
19630
+ function extractSkillLayer(raw) {
19631
+ return extractGenericLayer(raw, "skill");
19632
+ }
19633
+ function extractLayer(raw) {
19634
+ return extractGenericLayer(raw, "command");
19635
+ }
19576
19636
  function parseTrustedList(raw) {
19577
19637
  return raw.map((entry) => {
19578
19638
  if (typeof entry === "string") return { name: entry };
@@ -19753,6 +19813,17 @@ function mergeNonLayerFields(config, raw) {
19753
19813
  if (typeof raw.notifyOnDeny === "boolean") {
19754
19814
  config.notifyOnDeny = raw.notifyOnDeny;
19755
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
+ }
19756
19827
  if (raw.trustedContextOverrides && typeof raw.trustedContextOverrides === "object") {
19757
19828
  const overrides = raw.trustedContextOverrides;
19758
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
@@ -19522,6 +19568,14 @@ function loadConfig(cwd) {
19522
19568
  ...userLayer ? [userLayer] : [],
19523
19569
  defaultLayer
19524
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
+ ];
19525
19579
  if (userRaw) mergeNonLayerFields(config, userRaw);
19526
19580
  if (workspaceRaw) mergeNonLayerFields(config, workspaceRaw);
19527
19581
  return config;
@@ -19540,19 +19594,19 @@ function tryLoadFile(filePath) {
19540
19594
  }
19541
19595
  return null;
19542
19596
  }
19543
- function extractLayer(raw) {
19597
+ function extractGenericLayer(raw, nameKey) {
19544
19598
  const rules = Array.isArray(raw.rules) ? raw.rules : [];
19545
19599
  for (const rule of rules) {
19546
19600
  if (rule && typeof rule === "object") {
19547
19601
  if (rule.default && !isValidDecision(rule.default)) {
19548
- 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"
19549
19603
  `);
19550
19604
  rule.default = "ask";
19551
19605
  }
19552
19606
  if (Array.isArray(rule.argPatterns)) {
19553
19607
  for (const pattern of rule.argPatterns) {
19554
19608
  if (pattern?.decision && !isValidDecision(pattern.decision)) {
19555
- 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"
19556
19610
  `);
19557
19611
  pattern.decision = "ask";
19558
19612
  }
@@ -19566,6 +19620,12 @@ function extractLayer(raw) {
19566
19620
  rules
19567
19621
  };
19568
19622
  }
19623
+ function extractSkillLayer(raw) {
19624
+ return extractGenericLayer(raw, "skill");
19625
+ }
19626
+ function extractLayer(raw) {
19627
+ return extractGenericLayer(raw, "command");
19628
+ }
19569
19629
  function parseTrustedList(raw) {
19570
19630
  return raw.map((entry) => {
19571
19631
  if (typeof entry === "string") return { name: entry };
@@ -19746,6 +19806,17 @@ function mergeNonLayerFields(config, raw) {
19746
19806
  if (typeof raw.notifyOnDeny === "boolean") {
19747
19807
  config.notifyOnDeny = raw.notifyOnDeny;
19748
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
+ }
19749
19820
  if (raw.trustedContextOverrides && typeof raw.trustedContextOverrides === "object") {
19750
19821
  const overrides = raw.trustedContextOverrides;
19751
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
@@ -19522,6 +19568,14 @@ function loadConfig(cwd) {
19522
19568
  ...userLayer ? [userLayer] : [],
19523
19569
  defaultLayer
19524
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
+ ];
19525
19579
  if (userRaw) mergeNonLayerFields(config, userRaw);
19526
19580
  if (workspaceRaw) mergeNonLayerFields(config, workspaceRaw);
19527
19581
  return config;
@@ -19540,19 +19594,19 @@ function tryLoadFile(filePath) {
19540
19594
  }
19541
19595
  return null;
19542
19596
  }
19543
- function extractLayer(raw) {
19597
+ function extractGenericLayer(raw, nameKey) {
19544
19598
  const rules = Array.isArray(raw.rules) ? raw.rules : [];
19545
19599
  for (const rule of rules) {
19546
19600
  if (rule && typeof rule === "object") {
19547
19601
  if (rule.default && !isValidDecision(rule.default)) {
19548
- 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"
19549
19603
  `);
19550
19604
  rule.default = "ask";
19551
19605
  }
19552
19606
  if (Array.isArray(rule.argPatterns)) {
19553
19607
  for (const pattern of rule.argPatterns) {
19554
19608
  if (pattern?.decision && !isValidDecision(pattern.decision)) {
19555
- 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"
19556
19610
  `);
19557
19611
  pattern.decision = "ask";
19558
19612
  }
@@ -19566,6 +19620,12 @@ function extractLayer(raw) {
19566
19620
  rules
19567
19621
  };
19568
19622
  }
19623
+ function extractSkillLayer(raw) {
19624
+ return extractGenericLayer(raw, "skill");
19625
+ }
19626
+ function extractLayer(raw) {
19627
+ return extractGenericLayer(raw, "command");
19628
+ }
19569
19629
  function parseTrustedList(raw) {
19570
19630
  return raw.map((entry) => {
19571
19631
  if (typeof entry === "string") return { name: entry };
@@ -19746,6 +19806,17 @@ function mergeNonLayerFields(config, raw) {
19746
19806
  if (typeof raw.notifyOnDeny === "boolean") {
19747
19807
  config.notifyOnDeny = raw.notifyOnDeny;
19748
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
+ }
19749
19820
  if (raw.trustedContextOverrides && typeof raw.trustedContextOverrides === "object") {
19750
19821
  const overrides = raw.trustedContextOverrides;
19751
19822
  const layer = extractLayer(overrides);
@@ -20599,6 +20670,110 @@ function wardenEvalWithConfig(command, config, cwd) {
20599
20670
  return evaluate(parsed, config, cwd);
20600
20671
  }
20601
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
+
20602
20777
  // src/suggest.ts
20603
20778
  function generateAllowSnippet(details) {
20604
20779
  const lines = [];
@@ -20799,20 +20974,35 @@ function deactivateYolo(sessionId) {
20799
20974
 
20800
20975
  // src/index.ts
20801
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
+ }
20802
21000
  async function main() {
20803
21001
  let raw = "";
20804
21002
  for await (const chunk of process.stdin) {
20805
21003
  raw += chunk;
20806
21004
  if (raw.length > MAX_STDIN_SIZE) {
20807
- const output2 = {
20808
- hookSpecificOutput: {
20809
- hookEventName: "PreToolUse",
20810
- permissionDecision: "ask",
20811
- permissionDecisionReason: "[warden] Input exceeds size limit"
20812
- }
20813
- };
20814
- process.stdout.write(JSON.stringify(output2));
20815
- process.exit(0);
21005
+ emitDecision("ask", "[warden] Input exceeds size limit");
20816
21006
  }
20817
21007
  }
20818
21008
  let input;
@@ -20821,7 +21011,7 @@ async function main() {
20821
21011
  } catch {
20822
21012
  process.exit(0);
20823
21013
  }
20824
- if (input.tool_name !== "Bash") {
21014
+ if (input.tool_name !== "Bash" && input.tool_name !== "Skill") {
20825
21015
  process.exit(0);
20826
21016
  }
20827
21017
  if (input.permission_mode === "bypassPermissions") {
@@ -20830,100 +21020,63 @@ async function main() {
20830
21020
  if (process.env.WARDEN_YOLO === "true" || process.env.WARDEN_YOLO === "1") {
20831
21021
  process.exit(0);
20832
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
+ }
20833
21032
  const command = input.tool_input?.command;
20834
21033
  if (!command || typeof command !== "string") {
20835
21034
  process.exit(0);
20836
21035
  }
20837
21036
  const yoloCmd = parseYoloCommand(command);
20838
21037
  if (yoloCmd) {
20839
- let msg2;
21038
+ let msg;
20840
21039
  if (yoloCmd.action === "activate") {
20841
21040
  const state = activateYolo(input.session_id, yoloCmd.durationMinutes);
20842
21041
  const expiryInfo = state.expiresAt ? `expires at ${new Date(state.expiresAt).toLocaleTimeString()}` : "full session, no expiry";
20843
- 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.`;
20844
21043
  } else if (yoloCmd.action === "deactivate") {
20845
21044
  deactivateYolo(input.session_id);
20846
- msg2 = "[warden] YOLO mode deactivated. Normal rule evaluation resumed.";
21045
+ msg = "[warden] YOLO mode deactivated. Normal rule evaluation resumed.";
20847
21046
  } else {
20848
21047
  const state = getYoloState(input.session_id);
20849
21048
  if (state) {
20850
21049
  const expiryInfo = state.expiresAt ? `expires at ${new Date(state.expiresAt).toLocaleTimeString()}` : "full session";
20851
- msg2 = `[warden] YOLO mode is active (${expiryInfo})`;
21050
+ msg = `[warden] YOLO mode is active (${expiryInfo})`;
20852
21051
  } else {
20853
- msg2 = "[warden] YOLO mode is not active";
21052
+ msg = "[warden] YOLO mode is not active";
20854
21053
  }
20855
21054
  }
20856
- const output2 = {
20857
- hookSpecificOutput: {
20858
- hookEventName: "PreToolUse",
20859
- permissionDecision: "allow",
20860
- permissionDecisionReason: msg2
20861
- }
20862
- };
20863
- process.stdout.write(JSON.stringify(output2));
20864
- process.exit(0);
21055
+ emitDecision("allow", msg);
20865
21056
  }
20866
21057
  const config = loadConfig(input.cwd);
20867
21058
  const result = wardenEvalWithConfig(command, config, input.cwd);
20868
- const yoloState = getYoloState(input.session_id);
20869
- if (yoloState) {
20870
- if (result.decision === "deny" && !yoloState.bypassDeny) {
20871
- } else {
20872
- const expiryInfo = yoloState.expiresAt ? `expires ${new Date(yoloState.expiresAt).toLocaleTimeString()}` : "full session";
20873
- const output2 = {
20874
- hookSpecificOutput: {
20875
- hookEventName: "PreToolUse",
20876
- permissionDecision: "allow",
20877
- permissionDecisionReason: `[warden] YOLO mode active (${expiryInfo})`
20878
- }
20879
- };
20880
- process.stdout.write(JSON.stringify(output2));
20881
- process.exit(0);
20882
- }
20883
- }
21059
+ handleYoloMode(input.session_id, result);
21060
+ emitResult(result, command, config);
21061
+ }
21062
+ function emitResult(result, label, config) {
20884
21063
  if (result.decision === "allow") {
20885
- const output2 = {
20886
- hookSpecificOutput: {
20887
- hookEventName: "PreToolUse",
20888
- permissionDecision: "allow",
20889
- permissionDecisionReason: `[warden] ${result.reason}`
20890
- }
20891
- };
20892
- process.stdout.write(JSON.stringify(output2));
20893
- process.exit(0);
21064
+ emitDecision("allow", `[warden] ${result.reason}`);
20894
21065
  }
20895
21066
  if (result.decision === "deny") {
20896
21067
  if (config.notifyOnDeny) {
20897
- const truncated = command.length > 80 ? command.slice(0, 77) + "..." : command;
21068
+ const truncated = label.length > 80 ? label.slice(0, 77) + "..." : label;
20898
21069
  sendNotification("Claude Warden", `Blocked: ${truncated}`, config);
20899
21070
  }
20900
- const msg2 = formatSystemMessage("deny", command, result.details);
20901
- const output2 = {
20902
- hookSpecificOutput: {
20903
- hookEventName: "PreToolUse",
20904
- permissionDecision: "deny",
20905
- permissionDecisionReason: msg2
20906
- }
20907
- };
20908
- process.stdout.write(JSON.stringify(output2));
20909
- process.stderr.write(`[warden] Blocked: ${result.reason}
20910
- `);
20911
- process.exit(2);
21071
+ const msg2 = formatSystemMessage("deny", label, result.details);
21072
+ emitDecision("deny", msg2, `[warden] Blocked: ${result.reason}`);
20912
21073
  }
20913
21074
  if (config.notifyOnAsk) {
20914
- const truncated = command.length > 80 ? command.slice(0, 77) + "..." : command;
21075
+ const truncated = label.length > 80 ? label.slice(0, 77) + "..." : label;
20915
21076
  sendNotification("Claude Warden", `Permission needed: ${truncated}`, config);
20916
21077
  }
20917
- const msg = formatSystemMessage("ask", command, result.details);
20918
- const output = {
20919
- hookSpecificOutput: {
20920
- hookEventName: "PreToolUse",
20921
- permissionDecision: "ask",
20922
- permissionDecisionReason: msg
20923
- }
20924
- };
20925
- process.stdout.write(JSON.stringify(output));
20926
- process.exit(0);
21078
+ const msg = formatSystemMessage("ask", label, result.details);
21079
+ emitDecision("ask", msg);
20927
21080
  }
20928
21081
  main().catch(() => process.exit(0));
20929
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.6.0",
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",