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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/config/warden.default.yaml +42 -0
- package/dist/cli.cjs +98 -3
- package/dist/codex-export.cjs +98 -3
- package/dist/copilot.cjs +98 -3
- package/dist/index.cjs +269 -76
- package/hooks/hooks.json +22 -1
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "warden",
|
|
3
|
-
"version": "2.
|
|
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
|
|
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
|
|
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
|
|
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);
|
package/dist/codex-export.cjs
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
21090
|
+
msg = `[warden] YOLO mode is active (${expiryInfo})`;
|
|
20852
21091
|
} else {
|
|
20853
|
-
|
|
21092
|
+
msg = "[warden] YOLO mode is not active";
|
|
20854
21093
|
}
|
|
20855
21094
|
}
|
|
20856
|
-
|
|
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
|
-
|
|
20869
|
-
|
|
20870
|
-
|
|
20871
|
-
|
|
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
|
-
|
|
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 =
|
|
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",
|
|
20901
|
-
|
|
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 =
|
|
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",
|
|
20918
|
-
|
|
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
|
}
|