claude-warden 2.7.0 → 2.8.1

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.7.0",
11
+ "version": "2.8.1",
12
12
  "author": {
13
13
  "name": "banyudu"
14
14
  },
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "warden",
3
- "version": "2.7.0",
3
+ "version": "2.8.1",
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
package/dist/cli.cjs CHANGED
@@ -18860,6 +18860,91 @@ function registryOpsPattern() {
18860
18860
  reason: "Registry modification"
18861
18861
  };
18862
18862
  }
18863
+ var INLINE_LANG_CONFIG = {
18864
+ Python: {
18865
+ ext: "py",
18866
+ patterns: [
18867
+ "os\\.system",
18868
+ "subprocess",
18869
+ "commands\\.",
18870
+ "pty\\.",
18871
+ `__import__\\s*\\(\\s*['"](?:os|subprocess|socket)`,
18872
+ "\\bexec\\s*\\(",
18873
+ "\\beval\\s*\\(",
18874
+ `open\\s*\\([^)]*['"][wax+]`,
18875
+ "\\bsocket\\b",
18876
+ "urllib",
18877
+ "requests\\.",
18878
+ "http\\.client"
18879
+ ]
18880
+ },
18881
+ JavaScript: {
18882
+ ext: "js",
18883
+ patterns: [
18884
+ "child[_]process",
18885
+ `require\\s*\\(\\s*['"]child[_]process`,
18886
+ "\\.(?:writeFile|appendFile|createWriteStream|writeFileSync|appendFileSync)\\s*\\(",
18887
+ "http\\.request",
18888
+ "https\\.request",
18889
+ "net\\.(?:connect|createConnection)",
18890
+ "fetch\\s*\\("
18891
+ ]
18892
+ },
18893
+ Ruby: {
18894
+ ext: "rb",
18895
+ patterns: [
18896
+ "`",
18897
+ "%x[\\(\\{\\[]",
18898
+ "\\bsystem\\s*\\(",
18899
+ "\\bexec\\s*\\(",
18900
+ "IO\\.popen",
18901
+ "Kernel\\.",
18902
+ "\\bspawn\\s*\\(",
18903
+ `File\\.open\\s*\\([^)]*['"][wax+]`,
18904
+ "File\\.write",
18905
+ "open-uri",
18906
+ "Net::HTTP"
18907
+ ]
18908
+ },
18909
+ Perl: {
18910
+ ext: "pl",
18911
+ patterns: [
18912
+ "`",
18913
+ "qx[\\(\\{\\[/]",
18914
+ "\\bsystem\\s*\\(",
18915
+ "\\bexec\\s*\\(",
18916
+ `open\\s*\\([^)]*['"][>|+]`
18917
+ ]
18918
+ },
18919
+ PHP: {
18920
+ ext: "php",
18921
+ patterns: [
18922
+ "`",
18923
+ "shell_exec",
18924
+ "\\b(?:system|passthru|popen|proc_open)\\s*\\(",
18925
+ "\\bexec\\s*\\(",
18926
+ "file_put_contents",
18927
+ "fwrite",
18928
+ `fopen\\s*\\([^)]*['"][wax+]`,
18929
+ "curl_exec",
18930
+ "fsockopen"
18931
+ ]
18932
+ }
18933
+ };
18934
+ function inlineExecPatterns(lang, flags) {
18935
+ const { ext, patterns } = INLINE_LANG_CONFIG[lang];
18936
+ const reason = `Inline ${lang} is hard to audit. For JSON, prefer \`jq\`. For reuse, save to scripts/*.${ext} and run it.`;
18937
+ const flagAlt = flags.map((f) => f.replace(/^\^/, "").replace(/\$$/, "")).join("|");
18938
+ const compound = `(?:^|\\s)(?:${flagAlt})[\\s=][^\\n]{0,16000}?(?:${patterns.join("|")})`;
18939
+ return [
18940
+ { match: { argsMatch: [compound] }, decision: "ask", reason },
18941
+ {
18942
+ match: { anyArgMatches: flags },
18943
+ decision: "allow",
18944
+ description: `Plausibly read-only inline ${lang} script`
18945
+ }
18946
+ ];
18947
+ }
18863
18948
  function pkgManagerRule(command, extraSafeCmds = []) {
18864
18949
  const safeCmds = [...SAFE_PKG_MANAGER_CMDS, ...extraSafeCmds];
18865
18950
  return {
@@ -18933,6 +19018,18 @@ var DEFAULT_SKILL_RULES = {
18933
19018
  rules: []
18934
19019
  }]
18935
19020
  };
19021
+ var DEFAULT_TEMP_SCRIPT_DIR = "/tmp";
19022
+ function buildDefaultSessionGuidance(tempScriptDir) {
19023
+ return [
19024
+ "Claude Warden is active. It filters Bash commands against safety rules and may ask or deny.",
19025
+ "",
19026
+ "- For JSON in shell pipelines, prefer `jq` (auto-allowed) over `python3 -c` / `node -e`.",
19027
+ `- 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.`,
19028
+ "- When Warden denies or asks, read the reason \u2014 it often names the preferred alternative.",
19029
+ "- To permanently allow a specific command, run `/warden:allow <cmd>`. To temporarily bypass filtering, `/warden:yolo`."
19030
+ ].join("\n");
19031
+ }
19032
+ var DEFAULT_SESSION_GUIDANCE = buildDefaultSessionGuidance(DEFAULT_TEMP_SCRIPT_DIR);
18936
19033
  var DEFAULT_CONFIG = {
18937
19034
  defaultDecision: "ask",
18938
19035
  askOnSubshell: true,
@@ -19231,7 +19328,7 @@ var DEFAULT_CONFIG = {
19231
19328
  command: "node",
19232
19329
  default: "ask",
19233
19330
  argPatterns: [
19234
- { match: { anyArgMatches: ["^-e$", "^--eval", "^-p$", "^--print"] }, decision: "ask", reason: "Evaluating inline code" },
19331
+ ...inlineExecPatterns("JavaScript", ["^-e$", "^--eval", "^-p$", "^--print"]),
19235
19332
  { match: { anyArgMatches: ["^--(version|help)$", "^-[vh]$"] }, decision: "allow", description: "Version/help flags" },
19236
19333
  { match: { noArgs: true }, decision: "ask", reason: "Interactive REPL" }
19237
19334
  ]
@@ -19260,6 +19357,7 @@ var DEFAULT_CONFIG = {
19260
19357
  command: cmd,
19261
19358
  default: "ask",
19262
19359
  argPatterns: [
19360
+ ...inlineExecPatterns("Python", ["^-c$"]),
19263
19361
  { match: { anyArgMatches: ["^--(version|help)$", "^-V$"] }, decision: "allow" }
19264
19362
  ]
19265
19363
  })),
@@ -19418,14 +19516,9 @@ var DEFAULT_CONFIG = {
19418
19516
  argPatterns: [VERSION_HELP_FLAGS]
19419
19517
  })),
19420
19518
  // --- Scripting languages ---
19421
- ...["ruby", "perl", "php"].map((cmd) => ({
19422
- command: cmd,
19423
- default: "ask",
19424
- argPatterns: [
19425
- { match: { anyArgMatches: ["^-e$", "^--eval"] }, decision: "ask", reason: "Inline code execution" },
19426
- VERSION_HELP_FLAGS
19427
- ]
19428
- })),
19519
+ { command: "ruby", default: "ask", argPatterns: [...inlineExecPatterns("Ruby", ["^-e$", "^--eval"]), VERSION_HELP_FLAGS] },
19520
+ { command: "perl", default: "ask", argPatterns: [...inlineExecPatterns("Perl", ["^-e$", "^-E$"]), VERSION_HELP_FLAGS] },
19521
+ { command: "php", default: "ask", argPatterns: [...inlineExecPatterns("PHP", ["^-r$"]), VERSION_HELP_FLAGS] },
19429
19522
  // --- Java ecosystem ---
19430
19523
  { command: "java", default: "ask", argPatterns: [VERSION_HELP_FLAGS] },
19431
19524
  { command: "javac", default: "allow" },
@@ -19809,6 +19902,18 @@ function mergeNonLayerFields(config, raw) {
19809
19902
  if (typeof raw.notifyOnDeny === "boolean") {
19810
19903
  config.notifyOnDeny = raw.notifyOnDeny;
19811
19904
  }
19905
+ if (typeof raw.sessionGuidance === "string" || raw.sessionGuidance === false) {
19906
+ config.sessionGuidance = raw.sessionGuidance;
19907
+ } else if (raw.sessionGuidance !== void 0) {
19908
+ warn(`[warden] Warning: invalid sessionGuidance (expected string or false), ignoring
19909
+ `);
19910
+ }
19911
+ if (typeof raw.tempScriptDir === "string" && raw.tempScriptDir.length > 0) {
19912
+ config.tempScriptDir = raw.tempScriptDir;
19913
+ } else if (raw.tempScriptDir !== void 0) {
19914
+ warn(`[warden] Warning: invalid tempScriptDir (expected non-empty string), ignoring
19915
+ `);
19916
+ }
19812
19917
  if (raw.skills && typeof raw.skills === "object") {
19813
19918
  const skills = raw.skills;
19814
19919
  if (typeof skills.defaultDecision === "string") {
@@ -18864,6 +18864,91 @@ function registryOpsPattern() {
18864
18864
  reason: "Registry modification"
18865
18865
  };
18866
18866
  }
18867
+ var INLINE_LANG_CONFIG = {
18868
+ Python: {
18869
+ ext: "py",
18870
+ patterns: [
18871
+ "os\\.system",
18872
+ "subprocess",
18873
+ "commands\\.",
18874
+ "pty\\.",
18875
+ `__import__\\s*\\(\\s*['"](?:os|subprocess|socket)`,
18876
+ "\\bexec\\s*\\(",
18877
+ "\\beval\\s*\\(",
18878
+ `open\\s*\\([^)]*['"][wax+]`,
18879
+ "\\bsocket\\b",
18880
+ "urllib",
18881
+ "requests\\.",
18882
+ "http\\.client"
18883
+ ]
18884
+ },
18885
+ JavaScript: {
18886
+ ext: "js",
18887
+ patterns: [
18888
+ "child[_]process",
18889
+ `require\\s*\\(\\s*['"]child[_]process`,
18890
+ "\\.(?:writeFile|appendFile|createWriteStream|writeFileSync|appendFileSync)\\s*\\(",
18891
+ "http\\.request",
18892
+ "https\\.request",
18893
+ "net\\.(?:connect|createConnection)",
18894
+ "fetch\\s*\\("
18895
+ ]
18896
+ },
18897
+ Ruby: {
18898
+ ext: "rb",
18899
+ patterns: [
18900
+ "`",
18901
+ "%x[\\(\\{\\[]",
18902
+ "\\bsystem\\s*\\(",
18903
+ "\\bexec\\s*\\(",
18904
+ "IO\\.popen",
18905
+ "Kernel\\.",
18906
+ "\\bspawn\\s*\\(",
18907
+ `File\\.open\\s*\\([^)]*['"][wax+]`,
18908
+ "File\\.write",
18909
+ "open-uri",
18910
+ "Net::HTTP"
18911
+ ]
18912
+ },
18913
+ Perl: {
18914
+ ext: "pl",
18915
+ patterns: [
18916
+ "`",
18917
+ "qx[\\(\\{\\[/]",
18918
+ "\\bsystem\\s*\\(",
18919
+ "\\bexec\\s*\\(",
18920
+ `open\\s*\\([^)]*['"][>|+]`
18921
+ ]
18922
+ },
18923
+ PHP: {
18924
+ ext: "php",
18925
+ patterns: [
18926
+ "`",
18927
+ "shell_exec",
18928
+ "\\b(?:system|passthru|popen|proc_open)\\s*\\(",
18929
+ "\\bexec\\s*\\(",
18930
+ "file_put_contents",
18931
+ "fwrite",
18932
+ `fopen\\s*\\([^)]*['"][wax+]`,
18933
+ "curl_exec",
18934
+ "fsockopen"
18935
+ ]
18936
+ }
18937
+ };
18938
+ function inlineExecPatterns(lang, flags) {
18939
+ const { ext, patterns } = INLINE_LANG_CONFIG[lang];
18940
+ const reason = `Inline ${lang} is hard to audit. For JSON, prefer \`jq\`. For reuse, save to scripts/*.${ext} and run it.`;
18941
+ const flagAlt = flags.map((f) => f.replace(/^\^/, "").replace(/\$$/, "")).join("|");
18942
+ const compound = `(?:^|\\s)(?:${flagAlt})[\\s=][^\\n]{0,16000}?(?:${patterns.join("|")})`;
18943
+ return [
18944
+ { match: { argsMatch: [compound] }, decision: "ask", reason },
18945
+ {
18946
+ match: { anyArgMatches: flags },
18947
+ decision: "allow",
18948
+ description: `Plausibly read-only inline ${lang} script`
18949
+ }
18950
+ ];
18951
+ }
18867
18952
  function pkgManagerRule(command, extraSafeCmds = []) {
18868
18953
  const safeCmds = [...SAFE_PKG_MANAGER_CMDS, ...extraSafeCmds];
18869
18954
  return {
@@ -18937,6 +19022,18 @@ var DEFAULT_SKILL_RULES = {
18937
19022
  rules: []
18938
19023
  }]
18939
19024
  };
19025
+ var DEFAULT_TEMP_SCRIPT_DIR = "/tmp";
19026
+ function buildDefaultSessionGuidance(tempScriptDir) {
19027
+ return [
19028
+ "Claude Warden is active. It filters Bash commands against safety rules and may ask or deny.",
19029
+ "",
19030
+ "- For JSON in shell pipelines, prefer `jq` (auto-allowed) over `python3 -c` / `node -e`.",
19031
+ `- 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.`,
19032
+ "- When Warden denies or asks, read the reason \u2014 it often names the preferred alternative.",
19033
+ "- To permanently allow a specific command, run `/warden:allow <cmd>`. To temporarily bypass filtering, `/warden:yolo`."
19034
+ ].join("\n");
19035
+ }
19036
+ var DEFAULT_SESSION_GUIDANCE = buildDefaultSessionGuidance(DEFAULT_TEMP_SCRIPT_DIR);
18940
19037
  var DEFAULT_CONFIG = {
18941
19038
  defaultDecision: "ask",
18942
19039
  askOnSubshell: true,
@@ -19235,7 +19332,7 @@ var DEFAULT_CONFIG = {
19235
19332
  command: "node",
19236
19333
  default: "ask",
19237
19334
  argPatterns: [
19238
- { match: { anyArgMatches: ["^-e$", "^--eval", "^-p$", "^--print"] }, decision: "ask", reason: "Evaluating inline code" },
19335
+ ...inlineExecPatterns("JavaScript", ["^-e$", "^--eval", "^-p$", "^--print"]),
19239
19336
  { match: { anyArgMatches: ["^--(version|help)$", "^-[vh]$"] }, decision: "allow", description: "Version/help flags" },
19240
19337
  { match: { noArgs: true }, decision: "ask", reason: "Interactive REPL" }
19241
19338
  ]
@@ -19264,6 +19361,7 @@ var DEFAULT_CONFIG = {
19264
19361
  command: cmd,
19265
19362
  default: "ask",
19266
19363
  argPatterns: [
19364
+ ...inlineExecPatterns("Python", ["^-c$"]),
19267
19365
  { match: { anyArgMatches: ["^--(version|help)$", "^-V$"] }, decision: "allow" }
19268
19366
  ]
19269
19367
  })),
@@ -19422,14 +19520,9 @@ var DEFAULT_CONFIG = {
19422
19520
  argPatterns: [VERSION_HELP_FLAGS]
19423
19521
  })),
19424
19522
  // --- Scripting languages ---
19425
- ...["ruby", "perl", "php"].map((cmd) => ({
19426
- command: cmd,
19427
- default: "ask",
19428
- argPatterns: [
19429
- { match: { anyArgMatches: ["^-e$", "^--eval"] }, decision: "ask", reason: "Inline code execution" },
19430
- VERSION_HELP_FLAGS
19431
- ]
19432
- })),
19523
+ { command: "ruby", default: "ask", argPatterns: [...inlineExecPatterns("Ruby", ["^-e$", "^--eval"]), VERSION_HELP_FLAGS] },
19524
+ { command: "perl", default: "ask", argPatterns: [...inlineExecPatterns("Perl", ["^-e$", "^-E$"]), VERSION_HELP_FLAGS] },
19525
+ { command: "php", default: "ask", argPatterns: [...inlineExecPatterns("PHP", ["^-r$"]), VERSION_HELP_FLAGS] },
19433
19526
  // --- Java ecosystem ---
19434
19527
  { command: "java", default: "ask", argPatterns: [VERSION_HELP_FLAGS] },
19435
19528
  { command: "javac", default: "allow" },
@@ -19813,6 +19906,18 @@ function mergeNonLayerFields(config, raw) {
19813
19906
  if (typeof raw.notifyOnDeny === "boolean") {
19814
19907
  config.notifyOnDeny = raw.notifyOnDeny;
19815
19908
  }
19909
+ if (typeof raw.sessionGuidance === "string" || raw.sessionGuidance === false) {
19910
+ config.sessionGuidance = raw.sessionGuidance;
19911
+ } else if (raw.sessionGuidance !== void 0) {
19912
+ warn(`[warden] Warning: invalid sessionGuidance (expected string or false), ignoring
19913
+ `);
19914
+ }
19915
+ if (typeof raw.tempScriptDir === "string" && raw.tempScriptDir.length > 0) {
19916
+ config.tempScriptDir = raw.tempScriptDir;
19917
+ } else if (raw.tempScriptDir !== void 0) {
19918
+ warn(`[warden] Warning: invalid tempScriptDir (expected non-empty string), ignoring
19919
+ `);
19920
+ }
19816
19921
  if (raw.skills && typeof raw.skills === "object") {
19817
19922
  const skills = raw.skills;
19818
19923
  if (typeof skills.defaultDecision === "string") {
package/dist/copilot.cjs CHANGED
@@ -18860,6 +18860,91 @@ function registryOpsPattern() {
18860
18860
  reason: "Registry modification"
18861
18861
  };
18862
18862
  }
18863
+ var INLINE_LANG_CONFIG = {
18864
+ Python: {
18865
+ ext: "py",
18866
+ patterns: [
18867
+ "os\\.system",
18868
+ "subprocess",
18869
+ "commands\\.",
18870
+ "pty\\.",
18871
+ `__import__\\s*\\(\\s*['"](?:os|subprocess|socket)`,
18872
+ "\\bexec\\s*\\(",
18873
+ "\\beval\\s*\\(",
18874
+ `open\\s*\\([^)]*['"][wax+]`,
18875
+ "\\bsocket\\b",
18876
+ "urllib",
18877
+ "requests\\.",
18878
+ "http\\.client"
18879
+ ]
18880
+ },
18881
+ JavaScript: {
18882
+ ext: "js",
18883
+ patterns: [
18884
+ "child[_]process",
18885
+ `require\\s*\\(\\s*['"]child[_]process`,
18886
+ "\\.(?:writeFile|appendFile|createWriteStream|writeFileSync|appendFileSync)\\s*\\(",
18887
+ "http\\.request",
18888
+ "https\\.request",
18889
+ "net\\.(?:connect|createConnection)",
18890
+ "fetch\\s*\\("
18891
+ ]
18892
+ },
18893
+ Ruby: {
18894
+ ext: "rb",
18895
+ patterns: [
18896
+ "`",
18897
+ "%x[\\(\\{\\[]",
18898
+ "\\bsystem\\s*\\(",
18899
+ "\\bexec\\s*\\(",
18900
+ "IO\\.popen",
18901
+ "Kernel\\.",
18902
+ "\\bspawn\\s*\\(",
18903
+ `File\\.open\\s*\\([^)]*['"][wax+]`,
18904
+ "File\\.write",
18905
+ "open-uri",
18906
+ "Net::HTTP"
18907
+ ]
18908
+ },
18909
+ Perl: {
18910
+ ext: "pl",
18911
+ patterns: [
18912
+ "`",
18913
+ "qx[\\(\\{\\[/]",
18914
+ "\\bsystem\\s*\\(",
18915
+ "\\bexec\\s*\\(",
18916
+ `open\\s*\\([^)]*['"][>|+]`
18917
+ ]
18918
+ },
18919
+ PHP: {
18920
+ ext: "php",
18921
+ patterns: [
18922
+ "`",
18923
+ "shell_exec",
18924
+ "\\b(?:system|passthru|popen|proc_open)\\s*\\(",
18925
+ "\\bexec\\s*\\(",
18926
+ "file_put_contents",
18927
+ "fwrite",
18928
+ `fopen\\s*\\([^)]*['"][wax+]`,
18929
+ "curl_exec",
18930
+ "fsockopen"
18931
+ ]
18932
+ }
18933
+ };
18934
+ function inlineExecPatterns(lang, flags) {
18935
+ const { ext, patterns } = INLINE_LANG_CONFIG[lang];
18936
+ const reason = `Inline ${lang} is hard to audit. For JSON, prefer \`jq\`. For reuse, save to scripts/*.${ext} and run it.`;
18937
+ const flagAlt = flags.map((f) => f.replace(/^\^/, "").replace(/\$$/, "")).join("|");
18938
+ const compound = `(?:^|\\s)(?:${flagAlt})[\\s=][^\\n]{0,16000}?(?:${patterns.join("|")})`;
18939
+ return [
18940
+ { match: { argsMatch: [compound] }, decision: "ask", reason },
18941
+ {
18942
+ match: { anyArgMatches: flags },
18943
+ decision: "allow",
18944
+ description: `Plausibly read-only inline ${lang} script`
18945
+ }
18946
+ ];
18947
+ }
18863
18948
  function pkgManagerRule(command, extraSafeCmds = []) {
18864
18949
  const safeCmds = [...SAFE_PKG_MANAGER_CMDS, ...extraSafeCmds];
18865
18950
  return {
@@ -18933,6 +19018,18 @@ var DEFAULT_SKILL_RULES = {
18933
19018
  rules: []
18934
19019
  }]
18935
19020
  };
19021
+ var DEFAULT_TEMP_SCRIPT_DIR = "/tmp";
19022
+ function buildDefaultSessionGuidance(tempScriptDir) {
19023
+ return [
19024
+ "Claude Warden is active. It filters Bash commands against safety rules and may ask or deny.",
19025
+ "",
19026
+ "- For JSON in shell pipelines, prefer `jq` (auto-allowed) over `python3 -c` / `node -e`.",
19027
+ `- 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.`,
19028
+ "- When Warden denies or asks, read the reason \u2014 it often names the preferred alternative.",
19029
+ "- To permanently allow a specific command, run `/warden:allow <cmd>`. To temporarily bypass filtering, `/warden:yolo`."
19030
+ ].join("\n");
19031
+ }
19032
+ var DEFAULT_SESSION_GUIDANCE = buildDefaultSessionGuidance(DEFAULT_TEMP_SCRIPT_DIR);
18936
19033
  var DEFAULT_CONFIG = {
18937
19034
  defaultDecision: "ask",
18938
19035
  askOnSubshell: true,
@@ -19231,7 +19328,7 @@ var DEFAULT_CONFIG = {
19231
19328
  command: "node",
19232
19329
  default: "ask",
19233
19330
  argPatterns: [
19234
- { match: { anyArgMatches: ["^-e$", "^--eval", "^-p$", "^--print"] }, decision: "ask", reason: "Evaluating inline code" },
19331
+ ...inlineExecPatterns("JavaScript", ["^-e$", "^--eval", "^-p$", "^--print"]),
19235
19332
  { match: { anyArgMatches: ["^--(version|help)$", "^-[vh]$"] }, decision: "allow", description: "Version/help flags" },
19236
19333
  { match: { noArgs: true }, decision: "ask", reason: "Interactive REPL" }
19237
19334
  ]
@@ -19260,6 +19357,7 @@ var DEFAULT_CONFIG = {
19260
19357
  command: cmd,
19261
19358
  default: "ask",
19262
19359
  argPatterns: [
19360
+ ...inlineExecPatterns("Python", ["^-c$"]),
19263
19361
  { match: { anyArgMatches: ["^--(version|help)$", "^-V$"] }, decision: "allow" }
19264
19362
  ]
19265
19363
  })),
@@ -19418,14 +19516,9 @@ var DEFAULT_CONFIG = {
19418
19516
  argPatterns: [VERSION_HELP_FLAGS]
19419
19517
  })),
19420
19518
  // --- Scripting languages ---
19421
- ...["ruby", "perl", "php"].map((cmd) => ({
19422
- command: cmd,
19423
- default: "ask",
19424
- argPatterns: [
19425
- { match: { anyArgMatches: ["^-e$", "^--eval"] }, decision: "ask", reason: "Inline code execution" },
19426
- VERSION_HELP_FLAGS
19427
- ]
19428
- })),
19519
+ { command: "ruby", default: "ask", argPatterns: [...inlineExecPatterns("Ruby", ["^-e$", "^--eval"]), VERSION_HELP_FLAGS] },
19520
+ { command: "perl", default: "ask", argPatterns: [...inlineExecPatterns("Perl", ["^-e$", "^-E$"]), VERSION_HELP_FLAGS] },
19521
+ { command: "php", default: "ask", argPatterns: [...inlineExecPatterns("PHP", ["^-r$"]), VERSION_HELP_FLAGS] },
19429
19522
  // --- Java ecosystem ---
19430
19523
  { command: "java", default: "ask", argPatterns: [VERSION_HELP_FLAGS] },
19431
19524
  { command: "javac", default: "allow" },
@@ -19806,6 +19899,18 @@ function mergeNonLayerFields(config, raw) {
19806
19899
  if (typeof raw.notifyOnDeny === "boolean") {
19807
19900
  config.notifyOnDeny = raw.notifyOnDeny;
19808
19901
  }
19902
+ if (typeof raw.sessionGuidance === "string" || raw.sessionGuidance === false) {
19903
+ config.sessionGuidance = raw.sessionGuidance;
19904
+ } else if (raw.sessionGuidance !== void 0) {
19905
+ warn(`[warden] Warning: invalid sessionGuidance (expected string or false), ignoring
19906
+ `);
19907
+ }
19908
+ if (typeof raw.tempScriptDir === "string" && raw.tempScriptDir.length > 0) {
19909
+ config.tempScriptDir = raw.tempScriptDir;
19910
+ } else if (raw.tempScriptDir !== void 0) {
19911
+ warn(`[warden] Warning: invalid tempScriptDir (expected non-empty string), ignoring
19912
+ `);
19913
+ }
19809
19914
  if (raw.skills && typeof raw.skills === "object") {
19810
19915
  const skills = raw.skills;
19811
19916
  if (typeof skills.defaultDecision === "string") {
package/dist/index.cjs CHANGED
@@ -18860,6 +18860,91 @@ function registryOpsPattern() {
18860
18860
  reason: "Registry modification"
18861
18861
  };
18862
18862
  }
18863
+ var INLINE_LANG_CONFIG = {
18864
+ Python: {
18865
+ ext: "py",
18866
+ patterns: [
18867
+ "os\\.system",
18868
+ "subprocess",
18869
+ "commands\\.",
18870
+ "pty\\.",
18871
+ `__import__\\s*\\(\\s*['"](?:os|subprocess|socket)`,
18872
+ "\\bexec\\s*\\(",
18873
+ "\\beval\\s*\\(",
18874
+ `open\\s*\\([^)]*['"][wax+]`,
18875
+ "\\bsocket\\b",
18876
+ "urllib",
18877
+ "requests\\.",
18878
+ "http\\.client"
18879
+ ]
18880
+ },
18881
+ JavaScript: {
18882
+ ext: "js",
18883
+ patterns: [
18884
+ "child[_]process",
18885
+ `require\\s*\\(\\s*['"]child[_]process`,
18886
+ "\\.(?:writeFile|appendFile|createWriteStream|writeFileSync|appendFileSync)\\s*\\(",
18887
+ "http\\.request",
18888
+ "https\\.request",
18889
+ "net\\.(?:connect|createConnection)",
18890
+ "fetch\\s*\\("
18891
+ ]
18892
+ },
18893
+ Ruby: {
18894
+ ext: "rb",
18895
+ patterns: [
18896
+ "`",
18897
+ "%x[\\(\\{\\[]",
18898
+ "\\bsystem\\s*\\(",
18899
+ "\\bexec\\s*\\(",
18900
+ "IO\\.popen",
18901
+ "Kernel\\.",
18902
+ "\\bspawn\\s*\\(",
18903
+ `File\\.open\\s*\\([^)]*['"][wax+]`,
18904
+ "File\\.write",
18905
+ "open-uri",
18906
+ "Net::HTTP"
18907
+ ]
18908
+ },
18909
+ Perl: {
18910
+ ext: "pl",
18911
+ patterns: [
18912
+ "`",
18913
+ "qx[\\(\\{\\[/]",
18914
+ "\\bsystem\\s*\\(",
18915
+ "\\bexec\\s*\\(",
18916
+ `open\\s*\\([^)]*['"][>|+]`
18917
+ ]
18918
+ },
18919
+ PHP: {
18920
+ ext: "php",
18921
+ patterns: [
18922
+ "`",
18923
+ "shell_exec",
18924
+ "\\b(?:system|passthru|popen|proc_open)\\s*\\(",
18925
+ "\\bexec\\s*\\(",
18926
+ "file_put_contents",
18927
+ "fwrite",
18928
+ `fopen\\s*\\([^)]*['"][wax+]`,
18929
+ "curl_exec",
18930
+ "fsockopen"
18931
+ ]
18932
+ }
18933
+ };
18934
+ function inlineExecPatterns(lang, flags) {
18935
+ const { ext, patterns } = INLINE_LANG_CONFIG[lang];
18936
+ const reason = `Inline ${lang} is hard to audit. For JSON, prefer \`jq\`. For reuse, save to scripts/*.${ext} and run it.`;
18937
+ const flagAlt = flags.map((f) => f.replace(/^\^/, "").replace(/\$$/, "")).join("|");
18938
+ const compound = `(?:^|\\s)(?:${flagAlt})[\\s=][^\\n]{0,16000}?(?:${patterns.join("|")})`;
18939
+ return [
18940
+ { match: { argsMatch: [compound] }, decision: "ask", reason },
18941
+ {
18942
+ match: { anyArgMatches: flags },
18943
+ decision: "allow",
18944
+ description: `Plausibly read-only inline ${lang} script`
18945
+ }
18946
+ ];
18947
+ }
18863
18948
  function pkgManagerRule(command, extraSafeCmds = []) {
18864
18949
  const safeCmds = [...SAFE_PKG_MANAGER_CMDS, ...extraSafeCmds];
18865
18950
  return {
@@ -18933,6 +19018,18 @@ var DEFAULT_SKILL_RULES = {
18933
19018
  rules: []
18934
19019
  }]
18935
19020
  };
19021
+ var DEFAULT_TEMP_SCRIPT_DIR = "/tmp";
19022
+ function buildDefaultSessionGuidance(tempScriptDir) {
19023
+ return [
19024
+ "Claude Warden is active. It filters Bash commands against safety rules and may ask or deny.",
19025
+ "",
19026
+ "- For JSON in shell pipelines, prefer `jq` (auto-allowed) over `python3 -c` / `node -e`.",
19027
+ `- 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.`,
19028
+ "- When Warden denies or asks, read the reason \u2014 it often names the preferred alternative.",
19029
+ "- To permanently allow a specific command, run `/warden:allow <cmd>`. To temporarily bypass filtering, `/warden:yolo`."
19030
+ ].join("\n");
19031
+ }
19032
+ var DEFAULT_SESSION_GUIDANCE = buildDefaultSessionGuidance(DEFAULT_TEMP_SCRIPT_DIR);
18936
19033
  var DEFAULT_CONFIG = {
18937
19034
  defaultDecision: "ask",
18938
19035
  askOnSubshell: true,
@@ -19231,7 +19328,7 @@ var DEFAULT_CONFIG = {
19231
19328
  command: "node",
19232
19329
  default: "ask",
19233
19330
  argPatterns: [
19234
- { match: { anyArgMatches: ["^-e$", "^--eval", "^-p$", "^--print"] }, decision: "ask", reason: "Evaluating inline code" },
19331
+ ...inlineExecPatterns("JavaScript", ["^-e$", "^--eval", "^-p$", "^--print"]),
19235
19332
  { match: { anyArgMatches: ["^--(version|help)$", "^-[vh]$"] }, decision: "allow", description: "Version/help flags" },
19236
19333
  { match: { noArgs: true }, decision: "ask", reason: "Interactive REPL" }
19237
19334
  ]
@@ -19260,6 +19357,7 @@ var DEFAULT_CONFIG = {
19260
19357
  command: cmd,
19261
19358
  default: "ask",
19262
19359
  argPatterns: [
19360
+ ...inlineExecPatterns("Python", ["^-c$"]),
19263
19361
  { match: { anyArgMatches: ["^--(version|help)$", "^-V$"] }, decision: "allow" }
19264
19362
  ]
19265
19363
  })),
@@ -19418,14 +19516,9 @@ var DEFAULT_CONFIG = {
19418
19516
  argPatterns: [VERSION_HELP_FLAGS]
19419
19517
  })),
19420
19518
  // --- Scripting languages ---
19421
- ...["ruby", "perl", "php"].map((cmd) => ({
19422
- command: cmd,
19423
- default: "ask",
19424
- argPatterns: [
19425
- { match: { anyArgMatches: ["^-e$", "^--eval"] }, decision: "ask", reason: "Inline code execution" },
19426
- VERSION_HELP_FLAGS
19427
- ]
19428
- })),
19519
+ { command: "ruby", default: "ask", argPatterns: [...inlineExecPatterns("Ruby", ["^-e$", "^--eval"]), VERSION_HELP_FLAGS] },
19520
+ { command: "perl", default: "ask", argPatterns: [...inlineExecPatterns("Perl", ["^-e$", "^-E$"]), VERSION_HELP_FLAGS] },
19521
+ { command: "php", default: "ask", argPatterns: [...inlineExecPatterns("PHP", ["^-r$"]), VERSION_HELP_FLAGS] },
19429
19522
  // --- Java ecosystem ---
19430
19523
  { command: "java", default: "ask", argPatterns: [VERSION_HELP_FLAGS] },
19431
19524
  { command: "javac", default: "allow" },
@@ -19806,6 +19899,18 @@ function mergeNonLayerFields(config, raw) {
19806
19899
  if (typeof raw.notifyOnDeny === "boolean") {
19807
19900
  config.notifyOnDeny = raw.notifyOnDeny;
19808
19901
  }
19902
+ if (typeof raw.sessionGuidance === "string" || raw.sessionGuidance === false) {
19903
+ config.sessionGuidance = raw.sessionGuidance;
19904
+ } else if (raw.sessionGuidance !== void 0) {
19905
+ warn(`[warden] Warning: invalid sessionGuidance (expected string or false), ignoring
19906
+ `);
19907
+ }
19908
+ if (typeof raw.tempScriptDir === "string" && raw.tempScriptDir.length > 0) {
19909
+ config.tempScriptDir = raw.tempScriptDir;
19910
+ } else if (raw.tempScriptDir !== void 0) {
19911
+ warn(`[warden] Warning: invalid tempScriptDir (expected non-empty string), ignoring
19912
+ `);
19913
+ }
19809
19914
  if (raw.skills && typeof raw.skills === "object") {
19810
19915
  const skills = raw.skills;
19811
19916
  if (typeof skills.defaultDecision === "string") {
@@ -20990,6 +21095,18 @@ function emitDecision(decision, reason, stderrMessage) {
20990
21095
  }
20991
21096
  process.exit(0);
20992
21097
  }
21098
+ function handleSessionStart(config) {
21099
+ if (config.sessionGuidance === false) process.exit(0);
21100
+ const text = config.sessionGuidance ?? buildDefaultSessionGuidance(config.tempScriptDir ?? DEFAULT_TEMP_SCRIPT_DIR);
21101
+ const output = {
21102
+ hookSpecificOutput: {
21103
+ hookEventName: "SessionStart",
21104
+ additionalContext: text
21105
+ }
21106
+ };
21107
+ process.stdout.write(JSON.stringify(output));
21108
+ process.exit(0);
21109
+ }
20993
21110
  function handleYoloMode(sessionId, result) {
20994
21111
  const yoloState = getYoloState(sessionId);
20995
21112
  if (!yoloState) return;
@@ -21011,6 +21128,10 @@ async function main() {
21011
21128
  } catch {
21012
21129
  process.exit(0);
21013
21130
  }
21131
+ if (input.hook_event_name === "SessionStart") {
21132
+ const config2 = loadConfig(input.cwd);
21133
+ handleSessionStart(config2);
21134
+ }
21014
21135
  if (input.tool_name !== "Bash" && input.tool_name !== "Skill") {
21015
21136
  process.exit(0);
21016
21137
  }
package/hooks/hooks.json CHANGED
@@ -22,6 +22,17 @@
22
22
  }
23
23
  ]
24
24
  }
25
+ ],
26
+ "SessionStart": [
27
+ {
28
+ "hooks": [
29
+ {
30
+ "type": "command",
31
+ "command": "${CLAUDE_PLUGIN_ROOT}/dist/index.cjs",
32
+ "timeout": 5
33
+ }
34
+ ]
35
+ }
25
36
  ]
26
37
  }
27
38
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-warden",
3
- "version": "2.7.0",
3
+ "version": "2.8.1",
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",