vibesafu 0.1.26 → 0.1.27

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.
Files changed (2) hide show
  1. package/dist/index.js +132 -147
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -109,11 +109,7 @@ var DEFAULT_CONFIG = {
109
109
  block: [],
110
110
  allow: []
111
111
  },
112
- allowedMCPTools: [],
113
- logging: {
114
- enabled: true,
115
- path: join2(CONFIG_DIR, "logs")
116
- }
112
+ allowedMCPTools: []
117
113
  };
118
114
  function mergeConfig(defaults, user) {
119
115
  return {
@@ -121,8 +117,7 @@ function mergeConfig(defaults, user) {
121
117
  models: { ...defaults.models, ...user.models },
122
118
  trustedDomains: user.trustedDomains ?? defaults.trustedDomains,
123
119
  customPatterns: { ...defaults.customPatterns, ...user.customPatterns },
124
- allowedMCPTools: user.allowedMCPTools ?? defaults.allowedMCPTools,
125
- logging: { ...defaults.logging, ...user.logging }
120
+ allowedMCPTools: user.allowedMCPTools ?? defaults.allowedMCPTools
126
121
  };
127
122
  }
128
123
  async function readConfig() {
@@ -1562,19 +1557,21 @@ var PROMPT_INJECTION_PATTERNS = [
1562
1557
  /pretend\s+(to\s+be|you\s+are)/i,
1563
1558
  /new\s+instructions?:/i,
1564
1559
  /updated?\s+instructions?:/i,
1565
- // Context/role markers (could be trying to inject fake context)
1566
- /system\s*:/i,
1567
- /assistant\s*:/i,
1568
- /human\s*:/i,
1569
- /user\s*:/i,
1560
+ // Context/role markers (require injection-like context to avoid false positives)
1561
+ // "system:" alone is too broad (matches "operating system: linux")
1562
+ // Require either line-start or preceded by newline to indicate role-marker usage
1563
+ /^\s*system\s*:/im,
1564
+ /^\s*assistant\s*:/im,
1565
+ /^\s*human\s*:/im,
1566
+ /^\s*user\s*:/im,
1570
1567
  /<\s*system\s*>/i,
1571
1568
  /<\s*\/?\s*instructions?\s*>/i,
1572
- // Emphasis markers often used in injection
1573
- /\bIMPORTANT\s*:/i,
1574
- /\bNOTE\s*:/i,
1575
- /\bWARNING\s*:/i,
1576
- /\bCRITICAL\s*:/i,
1577
- /\bURGENT\s*:/i,
1569
+ // Emphasis markers - only flag when combined with directive language
1570
+ /\bIMPORTANT\s*:.*\b(approve|allow|safe|trust|skip|ignore)\b/i,
1571
+ /\bNOTE\s*:.*\b(approve|allow|safe|trust|skip|ignore)\b/i,
1572
+ /\bWARNING\s*:.*\b(approve|allow|safe|trust|skip|ignore)\b/i,
1573
+ /\bCRITICAL\s*:.*\b(approve|allow|safe|trust|skip|ignore)\b/i,
1574
+ /\bURGENT\s*:.*\b(approve|allow|safe|trust|skip|ignore)\b/i,
1578
1575
  // Output manipulation
1579
1576
  /respond\s+with\s+(this\s+)?(exact\s+)?json/i,
1580
1577
  /return\s+(only\s+)?["']?ALLOW["']?/i,
@@ -1634,10 +1631,10 @@ var FORCE_ESCALATE_PATTERNS = [
1634
1631
  // su commands
1635
1632
  /chmod\s+[0-7]*[7][0-7]*/i,
1636
1633
  // chmod with executable permissions
1637
- /\.env/i,
1638
- // env file access
1639
- /\/(etc|root|home)\//i
1640
- // System directory access
1634
+ /\.env(\s|$|\.local|\.production|\.development|\.staging|\.test)/i,
1635
+ // .env file access (not .envoy, .environment, etc.)
1636
+ /\/(etc|root)\//i
1637
+ // System directory access (/etc/, /root/ - not /home/ which is too broad)
1641
1638
  ];
1642
1639
  function shouldForceEscalate(command) {
1643
1640
  if (containsPromptInjection(command)) {
@@ -1702,6 +1699,40 @@ function extractJsonFromText(text) {
1702
1699
  return null;
1703
1700
  }
1704
1701
 
1702
+ // src/utils/llm-call.ts
1703
+ async function callLLM(options) {
1704
+ const { client, model, systemPrompt, userPrompt, maxTokens, timeoutMs } = options;
1705
+ try {
1706
+ const controller = new AbortController();
1707
+ const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
1708
+ const response = await client.messages.create(
1709
+ {
1710
+ model,
1711
+ max_tokens: maxTokens,
1712
+ system: systemPrompt,
1713
+ messages: [{ role: "user", content: userPrompt }]
1714
+ },
1715
+ { signal: controller.signal }
1716
+ );
1717
+ clearTimeout(timeoutId);
1718
+ const text = response.content[0]?.type === "text" ? response.content[0].text : "";
1719
+ if (!text) {
1720
+ return { ok: false, error: "empty_response", message: "Empty response from LLM" };
1721
+ }
1722
+ const extracted = extractJsonFromText(text);
1723
+ if (!extracted) {
1724
+ return { ok: false, error: "parse_error", message: "Could not parse JSON response" };
1725
+ }
1726
+ return { ok: true, data: extracted };
1727
+ } catch (error) {
1728
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
1729
+ if (errorMessage.includes("abort") || errorMessage.includes("timeout")) {
1730
+ return { ok: false, error: "timeout", message: "API timeout" };
1731
+ }
1732
+ return { ok: false, error: "api_error", message: errorMessage };
1733
+ }
1734
+ }
1735
+
1705
1736
  // src/guard/haiku-triage.ts
1706
1737
  var DEFAULT_HAIKU_MODEL = "claude-haiku-4-20250514";
1707
1738
  var API_TIMEOUT_MS = 3e4;
@@ -1760,70 +1791,42 @@ async function triageWithHaiku(client, checkpoint, model) {
1760
1791
  }
1761
1792
  const sanitizedCommand = sanitizeForPrompt(checkpoint.command);
1762
1793
  const userPrompt = TRIAGE_USER_PROMPT.replace("{command}", escapeXml(sanitizedCommand)).replace("{checkpoint_type}", escapeXml(checkpoint.type)).replace("{context}", escapeXml(checkpoint.description));
1763
- try {
1764
- const controller = new AbortController();
1765
- const timeoutId = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
1766
- const response = await client.messages.create(
1767
- {
1768
- model: model ?? DEFAULT_HAIKU_MODEL,
1769
- max_tokens: 500,
1770
- system: TRIAGE_SYSTEM_PROMPT,
1771
- messages: [{ role: "user", content: userPrompt }]
1772
- },
1773
- { signal: controller.signal }
1774
- );
1775
- clearTimeout(timeoutId);
1776
- const text = response.content[0]?.type === "text" ? response.content[0].text : "";
1777
- if (!text) {
1778
- return {
1779
- classification: "ESCALATE",
1780
- reason: "Triage failed: Empty response from Haiku",
1781
- riskIndicators: ["triage_error"]
1782
- };
1783
- }
1784
- const extracted = extractJsonFromText(text);
1785
- if (!extracted) {
1786
- return {
1787
- classification: "ESCALATE",
1788
- reason: "Triage failed: Could not parse JSON response",
1789
- riskIndicators: ["triage_error"]
1790
- };
1791
- }
1792
- const parsed = extracted;
1793
- if (!parsed.classification || !["SELF_HANDLE", "ESCALATE", "BLOCK"].includes(parsed.classification)) {
1794
- return {
1795
- classification: "ESCALATE",
1796
- reason: "Triage failed: Invalid classification in response",
1797
- riskIndicators: ["triage_error"]
1798
- };
1799
- }
1800
- if (parsed.classification === "SELF_HANDLE" && shouldForceEscalate(checkpoint.command)) {
1801
- return {
1802
- classification: "ESCALATE",
1803
- reason: "Auto-escalated: Command contains patterns requiring deeper review",
1804
- riskIndicators: ["forced_escalation", ...parsed.risk_indicators ?? []]
1805
- };
1806
- }
1794
+ const result = await callLLM({
1795
+ client,
1796
+ model: model ?? DEFAULT_HAIKU_MODEL,
1797
+ systemPrompt: TRIAGE_SYSTEM_PROMPT,
1798
+ userPrompt,
1799
+ maxTokens: 500,
1800
+ timeoutMs: API_TIMEOUT_MS
1801
+ });
1802
+ if (!result.ok) {
1803
+ const tag = result.error === "timeout" ? "triage_timeout" : "triage_error";
1807
1804
  return {
1808
- classification: parsed.classification,
1809
- reason: parsed.reason ?? "No reason provided",
1810
- riskIndicators: parsed.risk_indicators ?? []
1805
+ classification: "ESCALATE",
1806
+ reason: `Triage failed: ${result.message}`,
1807
+ riskIndicators: [tag]
1811
1808
  };
1812
- } catch (error) {
1813
- const errorMessage = error instanceof Error ? error.message : "Unknown error";
1814
- if (errorMessage.includes("abort") || errorMessage.includes("timeout")) {
1815
- return {
1816
- classification: "ESCALATE",
1817
- reason: "Triage failed: API timeout",
1818
- riskIndicators: ["triage_timeout"]
1819
- };
1820
- }
1809
+ }
1810
+ const parsed = result.data;
1811
+ if (!parsed.classification || !["SELF_HANDLE", "ESCALATE", "BLOCK"].includes(parsed.classification)) {
1821
1812
  return {
1822
1813
  classification: "ESCALATE",
1823
- reason: `Triage failed: ${errorMessage}`,
1814
+ reason: "Triage failed: Invalid classification in response",
1824
1815
  riskIndicators: ["triage_error"]
1825
1816
  };
1826
1817
  }
1818
+ if (parsed.classification === "SELF_HANDLE" && shouldForceEscalate(checkpoint.command)) {
1819
+ return {
1820
+ classification: "ESCALATE",
1821
+ reason: "Auto-escalated: Command contains patterns requiring deeper review",
1822
+ riskIndicators: ["forced_escalation", ...parsed.risk_indicators ?? []]
1823
+ };
1824
+ }
1825
+ return {
1826
+ classification: parsed.classification,
1827
+ reason: parsed.reason ?? "No reason provided",
1828
+ riskIndicators: parsed.risk_indicators ?? []
1829
+ };
1827
1830
  }
1828
1831
 
1829
1832
  // src/guard/sonnet-review.ts
@@ -1902,73 +1905,43 @@ BLOCK - Do not allow:
1902
1905
  async function reviewWithSonnet(client, checkpoint, triage, model) {
1903
1906
  const sanitizedCommand = sanitizeForPrompt(checkpoint.command);
1904
1907
  const userPrompt = REVIEW_USER_PROMPT.replace("{command}", escapeXml(sanitizedCommand)).replace("{checkpoint_type}", escapeXml(checkpoint.type)).replace("{context}", escapeXml(checkpoint.description)).replace("{triage_reason}", escapeXml(triage.reason)).replace("{risk_indicators}", escapeXml(triage.riskIndicators.join(", ") || "none"));
1905
- try {
1906
- const controller = new AbortController();
1907
- const timeoutId = setTimeout(() => controller.abort(), API_TIMEOUT_MS2);
1908
- const response = await client.messages.create(
1909
- {
1910
- model: model ?? DEFAULT_SONNET_MODEL,
1911
- max_tokens: 1e3,
1912
- system: REVIEW_SYSTEM_PROMPT,
1913
- messages: [{ role: "user", content: userPrompt }]
1914
- },
1915
- { signal: controller.signal }
1916
- );
1917
- clearTimeout(timeoutId);
1918
- const text = response.content[0]?.type === "text" ? response.content[0].text : "";
1919
- if (!text) {
1920
- return {
1921
- verdict: "ASK_USER",
1922
- riskLevel: "medium",
1923
- reason: "Review failed: Empty response from Sonnet",
1924
- userMessage: "Automated security review failed. Please review this operation manually."
1925
- };
1926
- }
1927
- const extracted = extractJsonFromText(text);
1928
- if (!extracted) {
1929
- return {
1930
- verdict: "ASK_USER",
1931
- riskLevel: "medium",
1932
- reason: "Review failed: Could not parse JSON response",
1933
- userMessage: "Automated security review failed. Please review this operation manually."
1934
- };
1935
- }
1936
- const parsed = extracted;
1937
- const verdict = parsed.verdict ?? "ASK_USER";
1938
- if (!["ALLOW", "ASK_USER", "BLOCK"].includes(verdict)) {
1939
- return {
1940
- verdict: "ASK_USER",
1941
- riskLevel: "medium",
1942
- reason: "Review failed: Invalid verdict in response",
1943
- userMessage: "Automated security review failed. Please review this operation manually."
1944
- };
1945
- }
1946
- const result = {
1947
- verdict,
1948
- riskLevel: parsed.risk_level ?? "medium",
1949
- reason: parsed.analysis?.intent ?? "Review completed"
1908
+ const FALLBACK_MSG = "Automated security review failed. Please review this operation manually.";
1909
+ const result = await callLLM({
1910
+ client,
1911
+ model: model ?? DEFAULT_SONNET_MODEL,
1912
+ systemPrompt: REVIEW_SYSTEM_PROMPT,
1913
+ userPrompt,
1914
+ maxTokens: 1e3,
1915
+ timeoutMs: API_TIMEOUT_MS2
1916
+ });
1917
+ if (!result.ok) {
1918
+ const msg = result.error === "timeout" ? "Security review timed out. Please review this operation manually." : FALLBACK_MSG;
1919
+ return {
1920
+ verdict: "ASK_USER",
1921
+ riskLevel: "medium",
1922
+ reason: `Review failed: ${result.message}`,
1923
+ userMessage: msg
1950
1924
  };
1951
- if (parsed.user_message) {
1952
- result.userMessage = parsed.user_message;
1953
- }
1954
- return result;
1955
- } catch (error) {
1956
- const errorMessage = error instanceof Error ? error.message : "Unknown error";
1957
- if (errorMessage.includes("abort") || errorMessage.includes("timeout")) {
1958
- return {
1959
- verdict: "ASK_USER",
1960
- riskLevel: "medium",
1961
- reason: "Review failed: API timeout",
1962
- userMessage: "Security review timed out. Please review this operation manually."
1963
- };
1964
- }
1925
+ }
1926
+ const parsed = result.data;
1927
+ const verdict = parsed.verdict ?? "ASK_USER";
1928
+ if (!["ALLOW", "ASK_USER", "BLOCK"].includes(verdict)) {
1965
1929
  return {
1966
1930
  verdict: "ASK_USER",
1967
1931
  riskLevel: "medium",
1968
- reason: `Review failed: ${errorMessage}`,
1969
- userMessage: "Automated security review failed. Please review this operation manually."
1932
+ reason: "Review failed: Invalid verdict in response",
1933
+ userMessage: FALLBACK_MSG
1970
1934
  };
1971
1935
  }
1936
+ const reviewResult = {
1937
+ verdict,
1938
+ riskLevel: parsed.risk_level ?? "medium",
1939
+ reason: parsed.analysis?.intent ?? "Review completed"
1940
+ };
1941
+ if (parsed.user_message) {
1942
+ reviewResult.userMessage = parsed.user_message;
1943
+ }
1944
+ return reviewResult;
1972
1945
  }
1973
1946
 
1974
1947
  // src/hook.ts
@@ -1976,12 +1949,22 @@ var TIMEOUT_SECONDS = 7;
1976
1949
  var REGEX_TIMEOUT_MS = 50;
1977
1950
  function safeRegexTest(pattern, input) {
1978
1951
  try {
1979
- const regex = new RegExp(pattern, "i");
1980
1952
  if (/(\(.+[+*]\))[+*]|\(\?:[^)]+[+*]\)[+*]/.test(pattern)) {
1981
1953
  process.stderr.write(`[vibesafu] Warning: Skipping potentially dangerous regex pattern: ${pattern}
1982
1954
  `);
1983
1955
  return false;
1984
1956
  }
1957
+ if (/\([^)]*\|[^)]*\)[+*]/.test(pattern)) {
1958
+ process.stderr.write(`[vibesafu] Warning: Skipping potentially dangerous regex pattern: ${pattern}
1959
+ `);
1960
+ return false;
1961
+ }
1962
+ if (/\([^)]*[+*][^)]*\)[+*]/.test(pattern)) {
1963
+ process.stderr.write(`[vibesafu] Warning: Skipping potentially dangerous regex pattern: ${pattern}
1964
+ `);
1965
+ return false;
1966
+ }
1967
+ const regex = new RegExp(pattern, "i");
1985
1968
  const testInput = input.length > REGEX_TIMEOUT_MS * 40 ? input.slice(0, REGEX_TIMEOUT_MS * 40) : input;
1986
1969
  return regex.test(testInput);
1987
1970
  } catch {
@@ -2098,6 +2081,13 @@ Auto-reject in ${TIMEOUT_SECONDS}s.`
2098
2081
  };
2099
2082
  }
2100
2083
  const command = input.tool_input.command;
2084
+ if (typeof command !== "string" || !command.trim()) {
2085
+ return {
2086
+ decision: "deny",
2087
+ reason: `Invalid input: Bash tool requires a non-empty string command, got ${typeof command}`,
2088
+ source: "instant-block"
2089
+ };
2090
+ }
2101
2091
  for (const pattern of config2.customPatterns.allow) {
2102
2092
  if (safeRegexTest(pattern, command)) {
2103
2093
  return {
@@ -2280,11 +2270,6 @@ If this was intentional, re-run the command and click "Allow".`;
2280
2270
  console.log(JSON.stringify(output));
2281
2271
  }
2282
2272
 
2283
- // src/cli/check.ts
2284
- async function check() {
2285
- await runHook();
2286
- }
2287
-
2288
2273
  // src/index.ts
2289
2274
  var __dirname = dirname(fileURLToPath(import.meta.url));
2290
2275
  var pkg = JSON.parse(readFileSync(join3(__dirname, "../package.json"), "utf-8"));
@@ -2323,7 +2308,7 @@ async function main() {
2323
2308
  await uninstall();
2324
2309
  break;
2325
2310
  case "check":
2326
- await check();
2311
+ await runHook();
2327
2312
  break;
2328
2313
  case "config":
2329
2314
  await config();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibesafu",
3
- "version": "0.1.26",
3
+ "version": "0.1.27",
4
4
  "description": "Better Claude Code workflow with smart safety checks. Safe YOLO mode without --dangerously-skip-permission",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",