claude-warden 2.6.0 → 2.8.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.8.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.8.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"
@@ -19,6 +19,26 @@ askOnSubshell: true
19
19
  notifyOnAsk: true
20
20
  notifyOnDeny: true
21
21
 
22
+ # Guidance injected into every Claude Code session via the SessionStart hook.
23
+ # Claude sees this as a system message, so it can shape tool choice *before*
24
+ # warden needs to ask or deny. Keep it short — it competes for context.
25
+ #
26
+ # - Omit the key to use the built-in default (prefer jq, save temp scripts to
27
+ # tempScriptDir below, read deny reasons, mention /warden:allow and /warden:yolo).
28
+ # - Set to a string to override with your own guidance.
29
+ # - Set to `false` to disable injection entirely.
30
+ #
31
+ # sessionGuidance: |
32
+ # Warden is active. Prefer allow-listed tools (e.g. jq for JSON). For
33
+ # multi-line logic, save a temp script under /tmp/ instead of using
34
+ # inline `bash -c` / `node -e`.
35
+
36
+ # Directory the built-in guidance tells Claude to save throwaway multi-line
37
+ # scripts to. Defaults to `/tmp` so scripts don't pollute the repo. Point at
38
+ # another location (e.g. `.warden-scratch`) if you want them tracked per-project.
39
+ # Only used when `sessionGuidance` is unset.
40
+ # tempScriptDir: /tmp
41
+
22
42
  # Additional commands to always allow (checked after alwaysDeny within this scope)
23
43
  # alwaysAllow:
24
44
  # - terraform
@@ -175,3 +195,25 @@ notifyOnDeny: true
175
195
  # anyArgMatches: ['^(ps|images|logs)$']
176
196
  # decision: allow
177
197
  # description: Read-only docker commands
198
+
199
+ # Skill (slash command) rules — filter Claude Code skill invocations.
200
+ # Skill names use the short form (e.g. "commit", not "/commit").
201
+ # Glob patterns supported for namespace matching (e.g. "example-plugin:*").
202
+ # Built-in defaults auto-allow common safe skills (commit, review, simplify, init).
203
+ # skills:
204
+ # defaultDecision: ask
205
+ # alwaysAllow:
206
+ # - commit
207
+ # - review
208
+ # - simplify
209
+ # - "example-plugin:*"
210
+ # alwaysDeny:
211
+ # - deploy
212
+ # rules:
213
+ # - skill: release
214
+ # default: ask
215
+ # argPatterns:
216
+ # - match:
217
+ # argsMatch: ["--dry-run"]
218
+ # decision: allow
219
+ # description: Dry-run release is safe
package/dist/cli.cjs CHANGED
@@ -18888,6 +18888,63 @@ 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
+ };
18936
+ var DEFAULT_TEMP_SCRIPT_DIR = "/tmp";
18937
+ function buildDefaultSessionGuidance(tempScriptDir) {
18938
+ return [
18939
+ "Claude Warden is active. It filters Bash commands against safety rules and may ask or deny.",
18940
+ "",
18941
+ "- For JSON in shell pipelines, prefer `jq` (auto-allowed) over `python3 -c` / `node -e`.",
18942
+ `- For multi-line logic, save a temp script under \`${tempScriptDir}/\` (e.g. \`${tempScriptDir}/warden-task.sh\`) or add a \`package.json\` script rather than inline \`bash -c\` / \`node -e\`. Avoid polluting the repo with throwaway scripts.`,
18943
+ "- When Warden denies or asks, read the reason \u2014 it often names the preferred alternative.",
18944
+ "- To permanently allow a specific command, run `/warden:allow <cmd>`. To temporarily bypass filtering, `/warden:yolo`."
18945
+ ].join("\n");
18946
+ }
18947
+ var DEFAULT_SESSION_GUIDANCE = buildDefaultSessionGuidance(DEFAULT_TEMP_SCRIPT_DIR);
18891
18948
  var DEFAULT_CONFIG = {
18892
18949
  defaultDecision: "ask",
18893
18950
  askOnSubshell: true,
@@ -18895,6 +18952,7 @@ var DEFAULT_CONFIG = {
18895
18952
  notifyOnDeny: true,
18896
18953
  trustedRemotes: [],
18897
18954
  targetPolicies: [],
18955
+ skillRules: DEFAULT_SKILL_RULES,
18898
18956
  layers: [{
18899
18957
  alwaysAllow: [
18900
18958
  // Read-only file operations
@@ -19525,6 +19583,14 @@ function loadConfig(cwd) {
19525
19583
  ...userLayer ? [userLayer] : [],
19526
19584
  defaultLayer
19527
19585
  ];
19586
+ const defaultSkillLayer = config.skillRules.layers[0];
19587
+ const userSkillLayer = userRaw?.skills ? extractSkillLayer(userRaw.skills) : null;
19588
+ const workspaceSkillLayer = workspaceRaw?.skills ? extractSkillLayer(workspaceRaw.skills) : null;
19589
+ config.skillRules.layers = [
19590
+ ...workspaceSkillLayer ? [workspaceSkillLayer] : [],
19591
+ ...userSkillLayer ? [userSkillLayer] : [],
19592
+ defaultSkillLayer
19593
+ ];
19528
19594
  if (userRaw) mergeNonLayerFields(config, userRaw);
19529
19595
  if (workspaceRaw) mergeNonLayerFields(config, workspaceRaw);
19530
19596
  return config;
@@ -19543,19 +19609,19 @@ function tryLoadFile(filePath) {
19543
19609
  }
19544
19610
  return null;
19545
19611
  }
19546
- function extractLayer(raw) {
19612
+ function extractGenericLayer(raw, nameKey) {
19547
19613
  const rules = Array.isArray(raw.rules) ? raw.rules : [];
19548
19614
  for (const rule of rules) {
19549
19615
  if (rule && typeof rule === "object") {
19550
19616
  if (rule.default && !isValidDecision(rule.default)) {
19551
- warn(`[warden] Warning: invalid rule default "${rule.default}" for "${rule.command}", using "ask"
19617
+ warn(`[warden] Warning: invalid rule default "${rule.default}" for "${rule[nameKey]}", using "ask"
19552
19618
  `);
19553
19619
  rule.default = "ask";
19554
19620
  }
19555
19621
  if (Array.isArray(rule.argPatterns)) {
19556
19622
  for (const pattern of rule.argPatterns) {
19557
19623
  if (pattern?.decision && !isValidDecision(pattern.decision)) {
19558
- warn(`[warden] Warning: invalid pattern decision "${pattern.decision}" for "${rule.command}", using "ask"
19624
+ warn(`[warden] Warning: invalid pattern decision "${pattern.decision}" for "${rule[nameKey]}", using "ask"
19559
19625
  `);
19560
19626
  pattern.decision = "ask";
19561
19627
  }
@@ -19569,6 +19635,12 @@ function extractLayer(raw) {
19569
19635
  rules
19570
19636
  };
19571
19637
  }
19638
+ function extractSkillLayer(raw) {
19639
+ return extractGenericLayer(raw, "skill");
19640
+ }
19641
+ function extractLayer(raw) {
19642
+ return extractGenericLayer(raw, "command");
19643
+ }
19572
19644
  function parseTrustedList(raw) {
19573
19645
  return raw.map((entry) => {
19574
19646
  if (typeof entry === "string") return { name: entry };
@@ -19749,6 +19821,29 @@ function mergeNonLayerFields(config, raw) {
19749
19821
  if (typeof raw.notifyOnDeny === "boolean") {
19750
19822
  config.notifyOnDeny = raw.notifyOnDeny;
19751
19823
  }
19824
+ if (typeof raw.sessionGuidance === "string" || raw.sessionGuidance === false) {
19825
+ config.sessionGuidance = raw.sessionGuidance;
19826
+ } else if (raw.sessionGuidance !== void 0) {
19827
+ warn(`[warden] Warning: invalid sessionGuidance (expected string or false), ignoring
19828
+ `);
19829
+ }
19830
+ if (typeof raw.tempScriptDir === "string" && raw.tempScriptDir.length > 0) {
19831
+ config.tempScriptDir = raw.tempScriptDir;
19832
+ } else if (raw.tempScriptDir !== void 0) {
19833
+ warn(`[warden] Warning: invalid tempScriptDir (expected non-empty string), ignoring
19834
+ `);
19835
+ }
19836
+ if (raw.skills && typeof raw.skills === "object") {
19837
+ const skills = raw.skills;
19838
+ if (typeof skills.defaultDecision === "string") {
19839
+ if (isValidDecision(skills.defaultDecision)) {
19840
+ config.skillRules.defaultDecision = skills.defaultDecision;
19841
+ } else {
19842
+ warn(`[warden] Warning: invalid skills.defaultDecision "${skills.defaultDecision}", ignoring
19843
+ `);
19844
+ }
19845
+ }
19846
+ }
19752
19847
  if (raw.trustedContextOverrides && typeof raw.trustedContextOverrides === "object") {
19753
19848
  const overrides = raw.trustedContextOverrides;
19754
19849
  const layer = extractLayer(overrides);
@@ -18892,6 +18892,63 @@ 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
+ };
18940
+ var DEFAULT_TEMP_SCRIPT_DIR = "/tmp";
18941
+ function buildDefaultSessionGuidance(tempScriptDir) {
18942
+ return [
18943
+ "Claude Warden is active. It filters Bash commands against safety rules and may ask or deny.",
18944
+ "",
18945
+ "- For JSON in shell pipelines, prefer `jq` (auto-allowed) over `python3 -c` / `node -e`.",
18946
+ `- For multi-line logic, save a temp script under \`${tempScriptDir}/\` (e.g. \`${tempScriptDir}/warden-task.sh\`) or add a \`package.json\` script rather than inline \`bash -c\` / \`node -e\`. Avoid polluting the repo with throwaway scripts.`,
18947
+ "- When Warden denies or asks, read the reason \u2014 it often names the preferred alternative.",
18948
+ "- To permanently allow a specific command, run `/warden:allow <cmd>`. To temporarily bypass filtering, `/warden:yolo`."
18949
+ ].join("\n");
18950
+ }
18951
+ var DEFAULT_SESSION_GUIDANCE = buildDefaultSessionGuidance(DEFAULT_TEMP_SCRIPT_DIR);
18895
18952
  var DEFAULT_CONFIG = {
18896
18953
  defaultDecision: "ask",
18897
18954
  askOnSubshell: true,
@@ -18899,6 +18956,7 @@ var DEFAULT_CONFIG = {
18899
18956
  notifyOnDeny: true,
18900
18957
  trustedRemotes: [],
18901
18958
  targetPolicies: [],
18959
+ skillRules: DEFAULT_SKILL_RULES,
18902
18960
  layers: [{
18903
18961
  alwaysAllow: [
18904
18962
  // Read-only file operations
@@ -19529,6 +19587,14 @@ function loadConfig(cwd) {
19529
19587
  ...userLayer ? [userLayer] : [],
19530
19588
  defaultLayer
19531
19589
  ];
19590
+ const defaultSkillLayer = config.skillRules.layers[0];
19591
+ const userSkillLayer = userRaw?.skills ? extractSkillLayer(userRaw.skills) : null;
19592
+ const workspaceSkillLayer = workspaceRaw?.skills ? extractSkillLayer(workspaceRaw.skills) : null;
19593
+ config.skillRules.layers = [
19594
+ ...workspaceSkillLayer ? [workspaceSkillLayer] : [],
19595
+ ...userSkillLayer ? [userSkillLayer] : [],
19596
+ defaultSkillLayer
19597
+ ];
19532
19598
  if (userRaw) mergeNonLayerFields(config, userRaw);
19533
19599
  if (workspaceRaw) mergeNonLayerFields(config, workspaceRaw);
19534
19600
  return config;
@@ -19547,19 +19613,19 @@ function tryLoadFile(filePath) {
19547
19613
  }
19548
19614
  return null;
19549
19615
  }
19550
- function extractLayer(raw) {
19616
+ function extractGenericLayer(raw, nameKey) {
19551
19617
  const rules = Array.isArray(raw.rules) ? raw.rules : [];
19552
19618
  for (const rule of rules) {
19553
19619
  if (rule && typeof rule === "object") {
19554
19620
  if (rule.default && !isValidDecision(rule.default)) {
19555
- warn(`[warden] Warning: invalid rule default "${rule.default}" for "${rule.command}", using "ask"
19621
+ warn(`[warden] Warning: invalid rule default "${rule.default}" for "${rule[nameKey]}", using "ask"
19556
19622
  `);
19557
19623
  rule.default = "ask";
19558
19624
  }
19559
19625
  if (Array.isArray(rule.argPatterns)) {
19560
19626
  for (const pattern of rule.argPatterns) {
19561
19627
  if (pattern?.decision && !isValidDecision(pattern.decision)) {
19562
- warn(`[warden] Warning: invalid pattern decision "${pattern.decision}" for "${rule.command}", using "ask"
19628
+ warn(`[warden] Warning: invalid pattern decision "${pattern.decision}" for "${rule[nameKey]}", using "ask"
19563
19629
  `);
19564
19630
  pattern.decision = "ask";
19565
19631
  }
@@ -19573,6 +19639,12 @@ function extractLayer(raw) {
19573
19639
  rules
19574
19640
  };
19575
19641
  }
19642
+ function extractSkillLayer(raw) {
19643
+ return extractGenericLayer(raw, "skill");
19644
+ }
19645
+ function extractLayer(raw) {
19646
+ return extractGenericLayer(raw, "command");
19647
+ }
19576
19648
  function parseTrustedList(raw) {
19577
19649
  return raw.map((entry) => {
19578
19650
  if (typeof entry === "string") return { name: entry };
@@ -19753,6 +19825,29 @@ function mergeNonLayerFields(config, raw) {
19753
19825
  if (typeof raw.notifyOnDeny === "boolean") {
19754
19826
  config.notifyOnDeny = raw.notifyOnDeny;
19755
19827
  }
19828
+ if (typeof raw.sessionGuidance === "string" || raw.sessionGuidance === false) {
19829
+ config.sessionGuidance = raw.sessionGuidance;
19830
+ } else if (raw.sessionGuidance !== void 0) {
19831
+ warn(`[warden] Warning: invalid sessionGuidance (expected string or false), ignoring
19832
+ `);
19833
+ }
19834
+ if (typeof raw.tempScriptDir === "string" && raw.tempScriptDir.length > 0) {
19835
+ config.tempScriptDir = raw.tempScriptDir;
19836
+ } else if (raw.tempScriptDir !== void 0) {
19837
+ warn(`[warden] Warning: invalid tempScriptDir (expected non-empty string), ignoring
19838
+ `);
19839
+ }
19840
+ if (raw.skills && typeof raw.skills === "object") {
19841
+ const skills = raw.skills;
19842
+ if (typeof skills.defaultDecision === "string") {
19843
+ if (isValidDecision(skills.defaultDecision)) {
19844
+ config.skillRules.defaultDecision = skills.defaultDecision;
19845
+ } else {
19846
+ warn(`[warden] Warning: invalid skills.defaultDecision "${skills.defaultDecision}", ignoring
19847
+ `);
19848
+ }
19849
+ }
19850
+ }
19756
19851
  if (raw.trustedContextOverrides && typeof raw.trustedContextOverrides === "object") {
19757
19852
  const overrides = raw.trustedContextOverrides;
19758
19853
  const layer = extractLayer(overrides);
package/dist/copilot.cjs CHANGED
@@ -18888,6 +18888,63 @@ 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
+ };
18936
+ var DEFAULT_TEMP_SCRIPT_DIR = "/tmp";
18937
+ function buildDefaultSessionGuidance(tempScriptDir) {
18938
+ return [
18939
+ "Claude Warden is active. It filters Bash commands against safety rules and may ask or deny.",
18940
+ "",
18941
+ "- For JSON in shell pipelines, prefer `jq` (auto-allowed) over `python3 -c` / `node -e`.",
18942
+ `- For multi-line logic, save a temp script under \`${tempScriptDir}/\` (e.g. \`${tempScriptDir}/warden-task.sh\`) or add a \`package.json\` script rather than inline \`bash -c\` / \`node -e\`. Avoid polluting the repo with throwaway scripts.`,
18943
+ "- When Warden denies or asks, read the reason \u2014 it often names the preferred alternative.",
18944
+ "- To permanently allow a specific command, run `/warden:allow <cmd>`. To temporarily bypass filtering, `/warden:yolo`."
18945
+ ].join("\n");
18946
+ }
18947
+ var DEFAULT_SESSION_GUIDANCE = buildDefaultSessionGuidance(DEFAULT_TEMP_SCRIPT_DIR);
18891
18948
  var DEFAULT_CONFIG = {
18892
18949
  defaultDecision: "ask",
18893
18950
  askOnSubshell: true,
@@ -18895,6 +18952,7 @@ var DEFAULT_CONFIG = {
18895
18952
  notifyOnDeny: true,
18896
18953
  trustedRemotes: [],
18897
18954
  targetPolicies: [],
18955
+ skillRules: DEFAULT_SKILL_RULES,
18898
18956
  layers: [{
18899
18957
  alwaysAllow: [
18900
18958
  // Read-only file operations
@@ -19522,6 +19580,14 @@ function loadConfig(cwd) {
19522
19580
  ...userLayer ? [userLayer] : [],
19523
19581
  defaultLayer
19524
19582
  ];
19583
+ const defaultSkillLayer = config.skillRules.layers[0];
19584
+ const userSkillLayer = userRaw?.skills ? extractSkillLayer(userRaw.skills) : null;
19585
+ const workspaceSkillLayer = workspaceRaw?.skills ? extractSkillLayer(workspaceRaw.skills) : null;
19586
+ config.skillRules.layers = [
19587
+ ...workspaceSkillLayer ? [workspaceSkillLayer] : [],
19588
+ ...userSkillLayer ? [userSkillLayer] : [],
19589
+ defaultSkillLayer
19590
+ ];
19525
19591
  if (userRaw) mergeNonLayerFields(config, userRaw);
19526
19592
  if (workspaceRaw) mergeNonLayerFields(config, workspaceRaw);
19527
19593
  return config;
@@ -19540,19 +19606,19 @@ function tryLoadFile(filePath) {
19540
19606
  }
19541
19607
  return null;
19542
19608
  }
19543
- function extractLayer(raw) {
19609
+ function extractGenericLayer(raw, nameKey) {
19544
19610
  const rules = Array.isArray(raw.rules) ? raw.rules : [];
19545
19611
  for (const rule of rules) {
19546
19612
  if (rule && typeof rule === "object") {
19547
19613
  if (rule.default && !isValidDecision(rule.default)) {
19548
- warn(`[warden] Warning: invalid rule default "${rule.default}" for "${rule.command}", using "ask"
19614
+ warn(`[warden] Warning: invalid rule default "${rule.default}" for "${rule[nameKey]}", using "ask"
19549
19615
  `);
19550
19616
  rule.default = "ask";
19551
19617
  }
19552
19618
  if (Array.isArray(rule.argPatterns)) {
19553
19619
  for (const pattern of rule.argPatterns) {
19554
19620
  if (pattern?.decision && !isValidDecision(pattern.decision)) {
19555
- warn(`[warden] Warning: invalid pattern decision "${pattern.decision}" for "${rule.command}", using "ask"
19621
+ warn(`[warden] Warning: invalid pattern decision "${pattern.decision}" for "${rule[nameKey]}", using "ask"
19556
19622
  `);
19557
19623
  pattern.decision = "ask";
19558
19624
  }
@@ -19566,6 +19632,12 @@ function extractLayer(raw) {
19566
19632
  rules
19567
19633
  };
19568
19634
  }
19635
+ function extractSkillLayer(raw) {
19636
+ return extractGenericLayer(raw, "skill");
19637
+ }
19638
+ function extractLayer(raw) {
19639
+ return extractGenericLayer(raw, "command");
19640
+ }
19569
19641
  function parseTrustedList(raw) {
19570
19642
  return raw.map((entry) => {
19571
19643
  if (typeof entry === "string") return { name: entry };
@@ -19746,6 +19818,29 @@ function mergeNonLayerFields(config, raw) {
19746
19818
  if (typeof raw.notifyOnDeny === "boolean") {
19747
19819
  config.notifyOnDeny = raw.notifyOnDeny;
19748
19820
  }
19821
+ if (typeof raw.sessionGuidance === "string" || raw.sessionGuidance === false) {
19822
+ config.sessionGuidance = raw.sessionGuidance;
19823
+ } else if (raw.sessionGuidance !== void 0) {
19824
+ warn(`[warden] Warning: invalid sessionGuidance (expected string or false), ignoring
19825
+ `);
19826
+ }
19827
+ if (typeof raw.tempScriptDir === "string" && raw.tempScriptDir.length > 0) {
19828
+ config.tempScriptDir = raw.tempScriptDir;
19829
+ } else if (raw.tempScriptDir !== void 0) {
19830
+ warn(`[warden] Warning: invalid tempScriptDir (expected non-empty string), ignoring
19831
+ `);
19832
+ }
19833
+ if (raw.skills && typeof raw.skills === "object") {
19834
+ const skills = raw.skills;
19835
+ if (typeof skills.defaultDecision === "string") {
19836
+ if (isValidDecision(skills.defaultDecision)) {
19837
+ config.skillRules.defaultDecision = skills.defaultDecision;
19838
+ } else {
19839
+ warn(`[warden] Warning: invalid skills.defaultDecision "${skills.defaultDecision}", ignoring
19840
+ `);
19841
+ }
19842
+ }
19843
+ }
19749
19844
  if (raw.trustedContextOverrides && typeof raw.trustedContextOverrides === "object") {
19750
19845
  const overrides = raw.trustedContextOverrides;
19751
19846
  const layer = extractLayer(overrides);
package/dist/index.cjs CHANGED
@@ -18888,6 +18888,63 @@ 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
+ };
18936
+ var DEFAULT_TEMP_SCRIPT_DIR = "/tmp";
18937
+ function buildDefaultSessionGuidance(tempScriptDir) {
18938
+ return [
18939
+ "Claude Warden is active. It filters Bash commands against safety rules and may ask or deny.",
18940
+ "",
18941
+ "- For JSON in shell pipelines, prefer `jq` (auto-allowed) over `python3 -c` / `node -e`.",
18942
+ `- For multi-line logic, save a temp script under \`${tempScriptDir}/\` (e.g. \`${tempScriptDir}/warden-task.sh\`) or add a \`package.json\` script rather than inline \`bash -c\` / \`node -e\`. Avoid polluting the repo with throwaway scripts.`,
18943
+ "- When Warden denies or asks, read the reason \u2014 it often names the preferred alternative.",
18944
+ "- To permanently allow a specific command, run `/warden:allow <cmd>`. To temporarily bypass filtering, `/warden:yolo`."
18945
+ ].join("\n");
18946
+ }
18947
+ var DEFAULT_SESSION_GUIDANCE = buildDefaultSessionGuidance(DEFAULT_TEMP_SCRIPT_DIR);
18891
18948
  var DEFAULT_CONFIG = {
18892
18949
  defaultDecision: "ask",
18893
18950
  askOnSubshell: true,
@@ -18895,6 +18952,7 @@ var DEFAULT_CONFIG = {
18895
18952
  notifyOnDeny: true,
18896
18953
  trustedRemotes: [],
18897
18954
  targetPolicies: [],
18955
+ skillRules: DEFAULT_SKILL_RULES,
18898
18956
  layers: [{
18899
18957
  alwaysAllow: [
18900
18958
  // Read-only file operations
@@ -19522,6 +19580,14 @@ function loadConfig(cwd) {
19522
19580
  ...userLayer ? [userLayer] : [],
19523
19581
  defaultLayer
19524
19582
  ];
19583
+ const defaultSkillLayer = config.skillRules.layers[0];
19584
+ const userSkillLayer = userRaw?.skills ? extractSkillLayer(userRaw.skills) : null;
19585
+ const workspaceSkillLayer = workspaceRaw?.skills ? extractSkillLayer(workspaceRaw.skills) : null;
19586
+ config.skillRules.layers = [
19587
+ ...workspaceSkillLayer ? [workspaceSkillLayer] : [],
19588
+ ...userSkillLayer ? [userSkillLayer] : [],
19589
+ defaultSkillLayer
19590
+ ];
19525
19591
  if (userRaw) mergeNonLayerFields(config, userRaw);
19526
19592
  if (workspaceRaw) mergeNonLayerFields(config, workspaceRaw);
19527
19593
  return config;
@@ -19540,19 +19606,19 @@ function tryLoadFile(filePath) {
19540
19606
  }
19541
19607
  return null;
19542
19608
  }
19543
- function extractLayer(raw) {
19609
+ function extractGenericLayer(raw, nameKey) {
19544
19610
  const rules = Array.isArray(raw.rules) ? raw.rules : [];
19545
19611
  for (const rule of rules) {
19546
19612
  if (rule && typeof rule === "object") {
19547
19613
  if (rule.default && !isValidDecision(rule.default)) {
19548
- warn(`[warden] Warning: invalid rule default "${rule.default}" for "${rule.command}", using "ask"
19614
+ warn(`[warden] Warning: invalid rule default "${rule.default}" for "${rule[nameKey]}", using "ask"
19549
19615
  `);
19550
19616
  rule.default = "ask";
19551
19617
  }
19552
19618
  if (Array.isArray(rule.argPatterns)) {
19553
19619
  for (const pattern of rule.argPatterns) {
19554
19620
  if (pattern?.decision && !isValidDecision(pattern.decision)) {
19555
- warn(`[warden] Warning: invalid pattern decision "${pattern.decision}" for "${rule.command}", using "ask"
19621
+ warn(`[warden] Warning: invalid pattern decision "${pattern.decision}" for "${rule[nameKey]}", using "ask"
19556
19622
  `);
19557
19623
  pattern.decision = "ask";
19558
19624
  }
@@ -19566,6 +19632,12 @@ function extractLayer(raw) {
19566
19632
  rules
19567
19633
  };
19568
19634
  }
19635
+ function extractSkillLayer(raw) {
19636
+ return extractGenericLayer(raw, "skill");
19637
+ }
19638
+ function extractLayer(raw) {
19639
+ return extractGenericLayer(raw, "command");
19640
+ }
19569
19641
  function parseTrustedList(raw) {
19570
19642
  return raw.map((entry) => {
19571
19643
  if (typeof entry === "string") return { name: entry };
@@ -19746,6 +19818,29 @@ function mergeNonLayerFields(config, raw) {
19746
19818
  if (typeof raw.notifyOnDeny === "boolean") {
19747
19819
  config.notifyOnDeny = raw.notifyOnDeny;
19748
19820
  }
19821
+ if (typeof raw.sessionGuidance === "string" || raw.sessionGuidance === false) {
19822
+ config.sessionGuidance = raw.sessionGuidance;
19823
+ } else if (raw.sessionGuidance !== void 0) {
19824
+ warn(`[warden] Warning: invalid sessionGuidance (expected string or false), ignoring
19825
+ `);
19826
+ }
19827
+ if (typeof raw.tempScriptDir === "string" && raw.tempScriptDir.length > 0) {
19828
+ config.tempScriptDir = raw.tempScriptDir;
19829
+ } else if (raw.tempScriptDir !== void 0) {
19830
+ warn(`[warden] Warning: invalid tempScriptDir (expected non-empty string), ignoring
19831
+ `);
19832
+ }
19833
+ if (raw.skills && typeof raw.skills === "object") {
19834
+ const skills = raw.skills;
19835
+ if (typeof skills.defaultDecision === "string") {
19836
+ if (isValidDecision(skills.defaultDecision)) {
19837
+ config.skillRules.defaultDecision = skills.defaultDecision;
19838
+ } else {
19839
+ warn(`[warden] Warning: invalid skills.defaultDecision "${skills.defaultDecision}", ignoring
19840
+ `);
19841
+ }
19842
+ }
19843
+ }
19749
19844
  if (raw.trustedContextOverrides && typeof raw.trustedContextOverrides === "object") {
19750
19845
  const overrides = raw.trustedContextOverrides;
19751
19846
  const layer = extractLayer(overrides);
@@ -20599,6 +20694,110 @@ function wardenEvalWithConfig(command, config, cwd) {
20599
20694
  return evaluate(parsed, config, cwd);
20600
20695
  }
20601
20696
 
20697
+ // src/skill-evaluator.ts
20698
+ function skillMatchesName(skillName, pattern) {
20699
+ return globToRegex(pattern).test(skillName);
20700
+ }
20701
+ function evaluateSkill(skillName, args2, config) {
20702
+ const detail = evaluateSkillDetail(skillName, args2, config);
20703
+ return {
20704
+ decision: detail.decision,
20705
+ reason: detail.reason,
20706
+ details: [detail]
20707
+ };
20708
+ }
20709
+ function evaluateSkillDetail(skillName, args2, config) {
20710
+ const { skillRules } = config;
20711
+ for (const layer of skillRules.layers) {
20712
+ if (layer.alwaysDeny.some((p) => skillMatchesName(skillName, p))) {
20713
+ return {
20714
+ command: skillName,
20715
+ args: args2 ? [args2] : [],
20716
+ decision: "deny",
20717
+ reason: `Skill "${skillName}" is blocked`,
20718
+ matchedRule: "alwaysDeny"
20719
+ };
20720
+ }
20721
+ if (layer.alwaysAllow.some((p) => skillMatchesName(skillName, p))) {
20722
+ return {
20723
+ command: skillName,
20724
+ args: args2 ? [args2] : [],
20725
+ decision: "allow",
20726
+ reason: `Skill "${skillName}" is safe`,
20727
+ matchedRule: "alwaysAllow"
20728
+ };
20729
+ }
20730
+ }
20731
+ const mergedRule = collectMergedSkillRule(skillName, skillRules.layers.map((l) => l.rules));
20732
+ if (mergedRule) {
20733
+ return evaluateSkillRule(skillName, args2, mergedRule);
20734
+ }
20735
+ return {
20736
+ command: skillName,
20737
+ args: args2 ? [args2] : [],
20738
+ decision: skillRules.defaultDecision,
20739
+ reason: `No rule for skill "${skillName}"`,
20740
+ matchedRule: "default"
20741
+ };
20742
+ }
20743
+ function collectMergedSkillRule(skillName, layerRules) {
20744
+ const matching = [];
20745
+ for (const rules of layerRules) {
20746
+ const rule = rules.find((r) => skillMatchesName(skillName, r.skill));
20747
+ if (rule) {
20748
+ matching.push(rule);
20749
+ if (rule.override) break;
20750
+ }
20751
+ }
20752
+ if (matching.length === 0) return null;
20753
+ if (matching.length === 1) return matching[0];
20754
+ const mergedPatterns = [];
20755
+ for (const rule of matching) {
20756
+ if (rule.argPatterns) {
20757
+ mergedPatterns.push(...rule.argPatterns);
20758
+ }
20759
+ }
20760
+ return {
20761
+ skill: matching[0].skill,
20762
+ default: matching[0].default,
20763
+ argPatterns: mergedPatterns
20764
+ };
20765
+ }
20766
+ function evaluateSkillRule(skillName, args2, rule) {
20767
+ const argsArray = args2 ? [args2] : [];
20768
+ const argsJoined = args2 || "";
20769
+ for (const pattern of rule.argPatterns || []) {
20770
+ const m = pattern.match;
20771
+ let matched = true;
20772
+ if (m.noArgs !== void 0) {
20773
+ matched = matched && m.noArgs === !args2;
20774
+ }
20775
+ if (m.argsMatch && matched) {
20776
+ matched = m.argsMatch.some((re) => safeRegexTest(re, argsJoined));
20777
+ }
20778
+ if (m.anyArgMatches && matched) {
20779
+ matched = argsArray.some((arg) => m.anyArgMatches.some((re) => safeRegexTest(re, arg)));
20780
+ }
20781
+ if (m.not) matched = !matched;
20782
+ if (matched) {
20783
+ return {
20784
+ command: skillName,
20785
+ args: argsArray,
20786
+ decision: pattern.decision,
20787
+ reason: pattern.reason || pattern.description || `Matched pattern for skill "${skillName}"`,
20788
+ matchedRule: `${skillName}:argPattern`
20789
+ };
20790
+ }
20791
+ }
20792
+ return {
20793
+ command: skillName,
20794
+ args: argsArray,
20795
+ decision: rule.default,
20796
+ reason: `Default for skill "${skillName}"`,
20797
+ matchedRule: `${skillName}:default`
20798
+ };
20799
+ }
20800
+
20602
20801
  // src/suggest.ts
20603
20802
  function generateAllowSnippet(details) {
20604
20803
  const lines = [];
@@ -20799,20 +20998,47 @@ function deactivateYolo(sessionId) {
20799
20998
 
20800
20999
  // src/index.ts
20801
21000
  var MAX_STDIN_SIZE = 1024 * 1024;
21001
+ function emitDecision(decision, reason, stderrMessage) {
21002
+ const output = {
21003
+ hookSpecificOutput: {
21004
+ hookEventName: "PreToolUse",
21005
+ permissionDecision: decision,
21006
+ permissionDecisionReason: reason
21007
+ }
21008
+ };
21009
+ process.stdout.write(JSON.stringify(output));
21010
+ if (decision === "deny") {
21011
+ process.stderr.write(`${stderrMessage ?? reason}
21012
+ `);
21013
+ process.exit(2);
21014
+ }
21015
+ process.exit(0);
21016
+ }
21017
+ function handleSessionStart(config) {
21018
+ if (config.sessionGuidance === false) process.exit(0);
21019
+ const text = config.sessionGuidance ?? buildDefaultSessionGuidance(config.tempScriptDir ?? DEFAULT_TEMP_SCRIPT_DIR);
21020
+ const output = {
21021
+ hookSpecificOutput: {
21022
+ hookEventName: "SessionStart",
21023
+ additionalContext: text
21024
+ }
21025
+ };
21026
+ process.stdout.write(JSON.stringify(output));
21027
+ process.exit(0);
21028
+ }
21029
+ function handleYoloMode(sessionId, result) {
21030
+ const yoloState = getYoloState(sessionId);
21031
+ if (!yoloState) return;
21032
+ if (result.decision === "deny" && !yoloState.bypassDeny) return;
21033
+ const expiryInfo = yoloState.expiresAt ? `expires ${new Date(yoloState.expiresAt).toLocaleTimeString()}` : "full session";
21034
+ emitDecision("allow", `[warden] YOLO mode active (${expiryInfo})`);
21035
+ }
20802
21036
  async function main() {
20803
21037
  let raw = "";
20804
21038
  for await (const chunk of process.stdin) {
20805
21039
  raw += chunk;
20806
21040
  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);
21041
+ emitDecision("ask", "[warden] Input exceeds size limit");
20816
21042
  }
20817
21043
  }
20818
21044
  let input;
@@ -20821,7 +21047,11 @@ async function main() {
20821
21047
  } catch {
20822
21048
  process.exit(0);
20823
21049
  }
20824
- if (input.tool_name !== "Bash") {
21050
+ if (input.hook_event_name === "SessionStart") {
21051
+ const config2 = loadConfig(input.cwd);
21052
+ handleSessionStart(config2);
21053
+ }
21054
+ if (input.tool_name !== "Bash" && input.tool_name !== "Skill") {
20825
21055
  process.exit(0);
20826
21056
  }
20827
21057
  if (input.permission_mode === "bypassPermissions") {
@@ -20830,100 +21060,63 @@ async function main() {
20830
21060
  if (process.env.WARDEN_YOLO === "true" || process.env.WARDEN_YOLO === "1") {
20831
21061
  process.exit(0);
20832
21062
  }
21063
+ if (input.tool_name === "Skill") {
21064
+ const skillName = input.tool_input?.skill;
21065
+ if (!skillName || typeof skillName !== "string") process.exit(0);
21066
+ const args2 = typeof input.tool_input?.args === "string" ? input.tool_input.args : void 0;
21067
+ const config2 = loadConfig(input.cwd);
21068
+ const result2 = evaluateSkill(skillName, args2, config2);
21069
+ handleYoloMode(input.session_id, result2);
21070
+ emitResult(result2, `skill:${skillName}`, config2);
21071
+ }
20833
21072
  const command = input.tool_input?.command;
20834
21073
  if (!command || typeof command !== "string") {
20835
21074
  process.exit(0);
20836
21075
  }
20837
21076
  const yoloCmd = parseYoloCommand(command);
20838
21077
  if (yoloCmd) {
20839
- let msg2;
21078
+ let msg;
20840
21079
  if (yoloCmd.action === "activate") {
20841
21080
  const state = activateYolo(input.session_id, yoloCmd.durationMinutes);
20842
21081
  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.`;
21082
+ msg = `[warden] YOLO mode activated (${expiryInfo}). Always-deny commands are still blocked. Use \`echo __WARDEN_YOLO_DEACTIVATE__\` to turn off.`;
20844
21083
  } else if (yoloCmd.action === "deactivate") {
20845
21084
  deactivateYolo(input.session_id);
20846
- msg2 = "[warden] YOLO mode deactivated. Normal rule evaluation resumed.";
21085
+ msg = "[warden] YOLO mode deactivated. Normal rule evaluation resumed.";
20847
21086
  } else {
20848
21087
  const state = getYoloState(input.session_id);
20849
21088
  if (state) {
20850
21089
  const expiryInfo = state.expiresAt ? `expires at ${new Date(state.expiresAt).toLocaleTimeString()}` : "full session";
20851
- msg2 = `[warden] YOLO mode is active (${expiryInfo})`;
21090
+ msg = `[warden] YOLO mode is active (${expiryInfo})`;
20852
21091
  } else {
20853
- msg2 = "[warden] YOLO mode is not active";
21092
+ msg = "[warden] YOLO mode is not active";
20854
21093
  }
20855
21094
  }
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);
21095
+ emitDecision("allow", msg);
20865
21096
  }
20866
21097
  const config = loadConfig(input.cwd);
20867
21098
  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
- }
21099
+ handleYoloMode(input.session_id, result);
21100
+ emitResult(result, command, config);
21101
+ }
21102
+ function emitResult(result, label, config) {
20884
21103
  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);
21104
+ emitDecision("allow", `[warden] ${result.reason}`);
20894
21105
  }
20895
21106
  if (result.decision === "deny") {
20896
21107
  if (config.notifyOnDeny) {
20897
- const truncated = command.length > 80 ? command.slice(0, 77) + "..." : command;
21108
+ const truncated = label.length > 80 ? label.slice(0, 77) + "..." : label;
20898
21109
  sendNotification("Claude Warden", `Blocked: ${truncated}`, config);
20899
21110
  }
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);
21111
+ const msg2 = formatSystemMessage("deny", label, result.details);
21112
+ emitDecision("deny", msg2, `[warden] Blocked: ${result.reason}`);
20912
21113
  }
20913
21114
  if (config.notifyOnAsk) {
20914
- const truncated = command.length > 80 ? command.slice(0, 77) + "..." : command;
21115
+ const truncated = label.length > 80 ? label.slice(0, 77) + "..." : label;
20915
21116
  sendNotification("Claude Warden", `Permission needed: ${truncated}`, config);
20916
21117
  }
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);
21118
+ const msg = formatSystemMessage("ask", label, result.details);
21119
+ emitDecision("ask", msg);
20927
21120
  }
20928
21121
  main().catch(() => process.exit(0));
20929
21122
  /*! 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,27 @@
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
+ ]
24
+ }
25
+ ],
26
+ "SessionStart": [
27
+ {
28
+ "hooks": [
29
+ {
30
+ "type": "command",
31
+ "command": "${CLAUDE_PLUGIN_ROOT}/dist/index.cjs",
32
+ "timeout": 5
33
+ }
34
+ ]
14
35
  }
15
36
  ]
16
37
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-warden",
3
- "version": "2.6.0",
3
+ "version": "2.8.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",