vibesafu 0.1.24 → 0.1.26

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 +200 -62
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -7,7 +7,7 @@ import { fileURLToPath } from "url";
7
7
  import { dirname, join as join3 } from "path";
8
8
 
9
9
  // src/cli/install.ts
10
- import { readFile, writeFile, mkdir } from "fs/promises";
10
+ import { readFile, writeFile, mkdir, chmod } from "fs/promises";
11
11
  import { homedir } from "os";
12
12
  import { join } from "path";
13
13
  var CLAUDE_SETTINGS_PATH = join(homedir(), ".claude", "settings.json");
@@ -24,7 +24,12 @@ async function readClaudeSettings() {
24
24
  try {
25
25
  const content = await readFile(CLAUDE_SETTINGS_PATH, "utf-8");
26
26
  return JSON.parse(content);
27
- } catch {
27
+ } catch (error) {
28
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
29
+ return {};
30
+ }
31
+ const msg = error instanceof Error ? error.message : "Unknown error";
32
+ console.error(`Warning: Failed to read Claude settings (${CLAUDE_SETTINGS_PATH}): ${msg}. Starting fresh.`);
28
33
  return {};
29
34
  }
30
35
  }
@@ -32,6 +37,7 @@ async function writeClaudeSettings(settings) {
32
37
  const dir = join(homedir(), ".claude");
33
38
  await mkdir(dir, { recursive: true });
34
39
  await writeFile(CLAUDE_SETTINGS_PATH, JSON.stringify(settings, null, 2));
40
+ await chmod(CLAUDE_SETTINGS_PATH, 384);
35
41
  }
36
42
  function isHookInstalled(settings) {
37
43
  const hooks = settings.hooks?.PermissionRequest ?? [];
@@ -84,7 +90,7 @@ async function uninstall() {
84
90
  }
85
91
 
86
92
  // src/cli/config.ts
87
- import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
93
+ import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2, chmod as chmod2 } from "fs/promises";
88
94
  import { homedir as homedir2 } from "os";
89
95
  import { join as join2 } from "path";
90
96
  import { createInterface } from "readline";
@@ -109,27 +115,44 @@ var DEFAULT_CONFIG = {
109
115
  path: join2(CONFIG_DIR, "logs")
110
116
  }
111
117
  };
118
+ function mergeConfig(defaults, user) {
119
+ return {
120
+ anthropic: { ...defaults.anthropic, ...user.anthropic },
121
+ models: { ...defaults.models, ...user.models },
122
+ trustedDomains: user.trustedDomains ?? defaults.trustedDomains,
123
+ customPatterns: { ...defaults.customPatterns, ...user.customPatterns },
124
+ allowedMCPTools: user.allowedMCPTools ?? defaults.allowedMCPTools,
125
+ logging: { ...defaults.logging, ...user.logging }
126
+ };
127
+ }
112
128
  async function readConfig() {
113
129
  try {
114
130
  const content = await readFile2(CONFIG_PATH, "utf-8");
115
- return { ...DEFAULT_CONFIG, ...JSON.parse(content) };
116
- } catch {
131
+ return mergeConfig(DEFAULT_CONFIG, JSON.parse(content));
132
+ } catch (error) {
133
+ if (error instanceof Error && "code" in error && error.code === "ENOENT") {
134
+ return DEFAULT_CONFIG;
135
+ }
136
+ const msg = error instanceof Error ? error.message : "Unknown error";
137
+ process.stderr.write(`[vibesafu] Warning: Failed to read config (${CONFIG_PATH}): ${msg}. Using defaults.
138
+ `);
117
139
  return DEFAULT_CONFIG;
118
140
  }
119
141
  }
120
142
  async function writeConfig(config2) {
121
143
  await mkdir2(CONFIG_DIR, { recursive: true });
122
144
  await writeFile2(CONFIG_PATH, JSON.stringify(config2, null, 2));
145
+ await chmod2(CONFIG_PATH, 384);
123
146
  }
124
147
  function prompt(question) {
125
148
  const rl = createInterface({
126
149
  input: process.stdin,
127
150
  output: process.stdout
128
151
  });
129
- return new Promise((resolve) => {
152
+ return new Promise((resolve2) => {
130
153
  rl.question(question, (answer) => {
131
154
  rl.close();
132
- resolve(answer);
155
+ resolve2(answer);
133
156
  });
134
157
  });
135
158
  }
@@ -155,16 +178,6 @@ async function config() {
155
178
  console.log("Configuration saved!");
156
179
  console.log(`Config file: ${CONFIG_PATH}`);
157
180
  }
158
- async function getApiKey() {
159
- if (process.env.ANTHROPIC_API_KEY) {
160
- return process.env.ANTHROPIC_API_KEY;
161
- }
162
- const cfg = await readConfig();
163
- if (cfg.anthropic.apiKey) {
164
- return cfg.anthropic.apiKey;
165
- }
166
- return void 0;
167
- }
168
181
 
169
182
  // src/hook.ts
170
183
  import Anthropic from "@anthropic-ai/sdk";
@@ -768,6 +781,13 @@ var INSTANT_BLOCK_PATTERNS = [
768
781
  ...DESTRUCTIVE_PATTERNS,
769
782
  ...SELF_PROTECTION_PATTERNS
770
783
  ];
784
+ for (const p of INSTANT_BLOCK_PATTERNS) {
785
+ if (p.pattern.global) {
786
+ throw new Error(
787
+ `Security pattern "${p.name}" must not use the global (g) flag. The g flag makes RegExp.test() stateful, causing intermittent bypasses. Remove the g flag from the pattern.`
788
+ );
789
+ }
790
+ }
771
791
  var CHECKPOINT_PATTERNS = [
772
792
  // Script execution
773
793
  { pattern: /curl\s+.*\|\s*(ba)?sh/i, type: "script_execution", description: "curl piped to shell" },
@@ -816,6 +836,13 @@ var CHECKPOINT_PATTERNS = [
816
836
  { pattern: /(cp|mv)\s+.*\.aws\//i, type: "file_sensitive", description: "Copying/moving AWS credentials" },
817
837
  { pattern: /(cp|mv)\s+.*\.env(\s|$)/i, type: "file_sensitive", description: "Copying/moving .env file" }
818
838
  ];
839
+ for (const p of CHECKPOINT_PATTERNS) {
840
+ if (p.pattern.global) {
841
+ throw new Error(
842
+ `Checkpoint pattern "${p.description}" must not use the global (g) flag. The g flag makes RegExp.test() stateful, causing intermittent bypasses. Remove the g flag from the pattern.`
843
+ );
844
+ }
845
+ }
819
846
 
820
847
  // src/guard/instant-block.ts
821
848
  function checkHighRiskPatterns(command) {
@@ -1022,7 +1049,10 @@ function isTrustedUrl(url) {
1022
1049
  function extractUrls(command) {
1023
1050
  const urlPattern = /https?:\/\/[^\s"'<>]+/gi;
1024
1051
  const matches = command.match(urlPattern);
1025
- return matches ?? [];
1052
+ if (!matches) return [];
1053
+ return matches.map((url) => {
1054
+ return url.replace(/[),;.]+$/, "");
1055
+ });
1026
1056
  }
1027
1057
  function isUrlShortener(hostname) {
1028
1058
  const normalizedHost = hostname.toLowerCase();
@@ -1046,6 +1076,21 @@ function containsUrlShortener(command) {
1046
1076
  shortenerUrls
1047
1077
  };
1048
1078
  }
1079
+ var RISKY_URL_PATTERNS = [
1080
+ // Raw file content from GitHub - can be any user's code
1081
+ /raw\.githubusercontent\.com/i,
1082
+ // GitHub gist raw content
1083
+ /gist\.github\.com\/[^/]+\/[^/]+\/raw/i,
1084
+ // GitHub releases downloads - binary files from any user
1085
+ /github\.com\/[^/]+\/[^/]+\/releases\/download/i,
1086
+ // GitHub objects (blobs, etc.)
1087
+ /objects\.githubusercontent\.com/i,
1088
+ // Installer script patterns (get.*.sh)
1089
+ /\/get\.[^/]+\.sh/i
1090
+ ];
1091
+ function isRiskyUrlPattern(url) {
1092
+ return RISKY_URL_PATTERNS.some((pattern) => pattern.test(url));
1093
+ }
1049
1094
 
1050
1095
  // src/guard/checkpoint.ts
1051
1096
  function detectCheckpoint(command) {
@@ -1079,29 +1124,39 @@ function checkTrustedDomains(command) {
1079
1124
  return {
1080
1125
  allTrusted: true,
1081
1126
  // No URLs means nothing untrusted
1127
+ hasRiskyUrls: false,
1082
1128
  urls: [],
1083
1129
  trustedUrls: [],
1084
- untrustedUrls: []
1130
+ untrustedUrls: [],
1131
+ riskyUrls: []
1085
1132
  };
1086
1133
  }
1087
1134
  const trustedUrls = [];
1088
1135
  const untrustedUrls = [];
1136
+ const riskyUrls = [];
1089
1137
  for (const url of urls) {
1090
1138
  if (isTrustedUrl(url)) {
1091
1139
  trustedUrls.push(url);
1092
1140
  } else {
1093
1141
  untrustedUrls.push(url);
1094
1142
  }
1143
+ if (isRiskyUrlPattern(url)) {
1144
+ riskyUrls.push(url);
1145
+ }
1095
1146
  }
1096
1147
  return {
1097
1148
  allTrusted: untrustedUrls.length === 0,
1149
+ hasRiskyUrls: riskyUrls.length > 0,
1098
1150
  urls,
1099
1151
  trustedUrls,
1100
- untrustedUrls
1152
+ untrustedUrls,
1153
+ riskyUrls
1101
1154
  };
1102
1155
  }
1103
1156
 
1104
1157
  // src/guard/file-tools.ts
1158
+ import { resolve } from "path";
1159
+ import { homedir as homedir3 } from "os";
1105
1160
  var WRITE_SENSITIVE_PATHS = [
1106
1161
  // SSH - Critical (persistent access)
1107
1162
  {
@@ -1431,8 +1486,18 @@ var READ_SENSITIVE_PATHS = [
1431
1486
  }
1432
1487
  ];
1433
1488
  function normalizePath(filePath) {
1434
- let normalized = filePath.replace(/\$HOME/g, "~").replace(/\$\{HOME\}/g, "~");
1489
+ let normalized = filePath;
1490
+ normalized = normalized.replace(/\$HOME/g, "~").replace(/\$\{HOME\}/g, "~");
1435
1491
  normalized = normalized.replace(/\/+/g, "/");
1492
+ if (normalized.startsWith("/")) {
1493
+ normalized = resolve(normalized);
1494
+ }
1495
+ const home = homedir3();
1496
+ if (normalized.startsWith(home + "/")) {
1497
+ normalized = "~" + normalized.slice(home.length);
1498
+ } else if (normalized === home) {
1499
+ normalized = "~";
1500
+ }
1436
1501
  return normalized;
1437
1502
  }
1438
1503
  function checkFilePath(filePath, action) {
@@ -1535,7 +1600,7 @@ function sanitizeForPrompt(command) {
1535
1600
  if (sanitized.length > MAX_COMMAND_LENGTH) {
1536
1601
  sanitized = sanitized.slice(0, MAX_COMMAND_LENGTH) + "... [truncated]";
1537
1602
  }
1538
- sanitized = sanitized.replace(/</g, "&lt;").replace(/>/g, "&gt;");
1603
+ sanitized = sanitized.replace(/]]>/g, "]]&gt;");
1539
1604
  sanitized = sanitized.replace(/\n{3,}/g, "\n\n");
1540
1605
  return sanitized;
1541
1606
  }
@@ -1580,9 +1645,65 @@ function shouldForceEscalate(command) {
1580
1645
  }
1581
1646
  return FORCE_ESCALATE_PATTERNS.some((pattern) => pattern.test(command));
1582
1647
  }
1648
+ function extractJsonFromText(text) {
1649
+ try {
1650
+ const parsed = JSON.parse(text.trim());
1651
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
1652
+ return parsed;
1653
+ }
1654
+ } catch {
1655
+ }
1656
+ const codeBlockMatch = text.match(/```(?:json)?\s*\n?([\s\S]*?)\n?\s*```/);
1657
+ if (codeBlockMatch) {
1658
+ try {
1659
+ const parsed = JSON.parse(codeBlockMatch[1].trim());
1660
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
1661
+ return parsed;
1662
+ }
1663
+ } catch {
1664
+ }
1665
+ }
1666
+ const startIdx = text.indexOf("{");
1667
+ if (startIdx === -1) return null;
1668
+ let depth = 0;
1669
+ let inString = false;
1670
+ let escaped = false;
1671
+ for (let i = startIdx; i < text.length; i++) {
1672
+ const ch = text[i];
1673
+ if (escaped) {
1674
+ escaped = false;
1675
+ continue;
1676
+ }
1677
+ if (ch === "\\" && inString) {
1678
+ escaped = true;
1679
+ continue;
1680
+ }
1681
+ if (ch === '"') {
1682
+ inString = !inString;
1683
+ continue;
1684
+ }
1685
+ if (inString) continue;
1686
+ if (ch === "{") depth++;
1687
+ else if (ch === "}") {
1688
+ depth--;
1689
+ if (depth === 0) {
1690
+ const candidate = text.slice(startIdx, i + 1);
1691
+ try {
1692
+ const parsed = JSON.parse(candidate);
1693
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
1694
+ return parsed;
1695
+ }
1696
+ } catch {
1697
+ return null;
1698
+ }
1699
+ }
1700
+ }
1701
+ }
1702
+ return null;
1703
+ }
1583
1704
 
1584
1705
  // src/guard/haiku-triage.ts
1585
- var HAIKU_MODEL = "claude-haiku-4-20250514";
1706
+ var DEFAULT_HAIKU_MODEL = "claude-haiku-4-20250514";
1586
1707
  var API_TIMEOUT_MS = 3e4;
1587
1708
  var TRIAGE_SYSTEM_PROMPT = `You are a security triage agent for an autonomous coding system.
1588
1709
  Your ONLY job is to classify commands as SELF_HANDLE, ESCALATE, or BLOCK.
@@ -1629,7 +1750,7 @@ var FORCE_ESCALATE_TYPES = [
1629
1750
  "package_install"
1630
1751
  // Supply chain attacks via postinstall scripts
1631
1752
  ];
1632
- async function triageWithHaiku(client, checkpoint) {
1753
+ async function triageWithHaiku(client, checkpoint, model) {
1633
1754
  if (FORCE_ESCALATE_TYPES.includes(checkpoint.type)) {
1634
1755
  return {
1635
1756
  classification: "ESCALATE",
@@ -1644,7 +1765,7 @@ async function triageWithHaiku(client, checkpoint) {
1644
1765
  const timeoutId = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
1645
1766
  const response = await client.messages.create(
1646
1767
  {
1647
- model: HAIKU_MODEL,
1768
+ model: model ?? DEFAULT_HAIKU_MODEL,
1648
1769
  max_tokens: 500,
1649
1770
  system: TRIAGE_SYSTEM_PROMPT,
1650
1771
  messages: [{ role: "user", content: userPrompt }]
@@ -1660,15 +1781,15 @@ async function triageWithHaiku(client, checkpoint) {
1660
1781
  riskIndicators: ["triage_error"]
1661
1782
  };
1662
1783
  }
1663
- const jsonMatch = text.match(/\{[\s\S]*\}/);
1664
- if (!jsonMatch) {
1784
+ const extracted = extractJsonFromText(text);
1785
+ if (!extracted) {
1665
1786
  return {
1666
1787
  classification: "ESCALATE",
1667
1788
  reason: "Triage failed: Could not parse JSON response",
1668
1789
  riskIndicators: ["triage_error"]
1669
1790
  };
1670
1791
  }
1671
- const parsed = JSON.parse(jsonMatch[0]);
1792
+ const parsed = extracted;
1672
1793
  if (!parsed.classification || !["SELF_HANDLE", "ESCALATE", "BLOCK"].includes(parsed.classification)) {
1673
1794
  return {
1674
1795
  classification: "ESCALATE",
@@ -1706,7 +1827,7 @@ async function triageWithHaiku(client, checkpoint) {
1706
1827
  }
1707
1828
 
1708
1829
  // src/guard/sonnet-review.ts
1709
- var SONNET_MODEL = "claude-sonnet-4-20250514";
1830
+ var DEFAULT_SONNET_MODEL = "claude-sonnet-4-20250514";
1710
1831
  var API_TIMEOUT_MS2 = 6e4;
1711
1832
  var REVIEW_SYSTEM_PROMPT = `You are a senior security engineer reviewing potentially risky operations.
1712
1833
  Your job is to analyze commands and determine if they are safe to execute.
@@ -1778,7 +1899,7 @@ BLOCK - Do not allow:
1778
1899
  "user_message": "Concise message explaining the security risk to the user (2-3 sentences max). Do NOT include timing or instructions - those are added automatically."
1779
1900
  }
1780
1901
  </response_format>`;
1781
- async function reviewWithSonnet(client, checkpoint, triage) {
1902
+ async function reviewWithSonnet(client, checkpoint, triage, model) {
1782
1903
  const sanitizedCommand = sanitizeForPrompt(checkpoint.command);
1783
1904
  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"));
1784
1905
  try {
@@ -1786,7 +1907,7 @@ async function reviewWithSonnet(client, checkpoint, triage) {
1786
1907
  const timeoutId = setTimeout(() => controller.abort(), API_TIMEOUT_MS2);
1787
1908
  const response = await client.messages.create(
1788
1909
  {
1789
- model: SONNET_MODEL,
1910
+ model: model ?? DEFAULT_SONNET_MODEL,
1790
1911
  max_tokens: 1e3,
1791
1912
  system: REVIEW_SYSTEM_PROMPT,
1792
1913
  messages: [{ role: "user", content: userPrompt }]
@@ -1803,8 +1924,8 @@ async function reviewWithSonnet(client, checkpoint, triage) {
1803
1924
  userMessage: "Automated security review failed. Please review this operation manually."
1804
1925
  };
1805
1926
  }
1806
- const jsonMatch = text.match(/\{[\s\S]*\}/);
1807
- if (!jsonMatch) {
1927
+ const extracted = extractJsonFromText(text);
1928
+ if (!extracted) {
1808
1929
  return {
1809
1930
  verdict: "ASK_USER",
1810
1931
  riskLevel: "medium",
@@ -1812,7 +1933,7 @@ async function reviewWithSonnet(client, checkpoint, triage) {
1812
1933
  userMessage: "Automated security review failed. Please review this operation manually."
1813
1934
  };
1814
1935
  }
1815
- const parsed = JSON.parse(jsonMatch[0]);
1936
+ const parsed = extracted;
1816
1937
  const verdict = parsed.verdict ?? "ASK_USER";
1817
1938
  if (!["ALLOW", "ASK_USER", "BLOCK"].includes(verdict)) {
1818
1939
  return {
@@ -1852,9 +1973,25 @@ async function reviewWithSonnet(client, checkpoint, triage) {
1852
1973
 
1853
1974
  // src/hook.ts
1854
1975
  var TIMEOUT_SECONDS = 7;
1976
+ var REGEX_TIMEOUT_MS = 50;
1977
+ function safeRegexTest(pattern, input) {
1978
+ try {
1979
+ const regex = new RegExp(pattern, "i");
1980
+ if (/(\(.+[+*]\))[+*]|\(\?:[^)]+[+*]\)[+*]/.test(pattern)) {
1981
+ process.stderr.write(`[vibesafu] Warning: Skipping potentially dangerous regex pattern: ${pattern}
1982
+ `);
1983
+ return false;
1984
+ }
1985
+ const testInput = input.length > REGEX_TIMEOUT_MS * 40 ? input.slice(0, REGEX_TIMEOUT_MS * 40) : input;
1986
+ return regex.test(testInput);
1987
+ } catch {
1988
+ return false;
1989
+ }
1990
+ }
1855
1991
  var PLAN_MODE_TIMEOUT_SECONDS = 72 * 60 * 60;
1856
1992
  var SAFE_NON_BASH_TOOLS = ["WebFetch", "WebSearch", "Task", "Glob", "Grep", "LS", "TodoRead", "TodoWrite", "NotebookRead"];
1857
- async function processPermissionRequest(input, anthropicClient) {
1993
+ async function processPermissionRequest(input, anthropicClient, preloadedConfig) {
1994
+ const config2 = preloadedConfig ?? await readConfig();
1858
1995
  if (input.tool_name === "Write" || input.tool_name === "Edit" || input.tool_name === "Read") {
1859
1996
  const fileCheck = checkFileTool(input.tool_name, input.tool_input);
1860
1997
  if (fileCheck.blocked) {
@@ -1917,8 +2054,7 @@ This will auto-reject if not approved.`,
1917
2054
  };
1918
2055
  }
1919
2056
  if (input.tool_name.startsWith("mcp__")) {
1920
- const config3 = await readConfig();
1921
- const isAllowed = config3.allowedMCPTools.some((pattern) => {
2057
+ const isAllowed = config2.allowedMCPTools.some((pattern) => {
1922
2058
  if (pattern.endsWith("*")) {
1923
2059
  const prefix = pattern.slice(0, -1);
1924
2060
  return input.tool_name.startsWith(prefix);
@@ -1962,34 +2098,27 @@ Auto-reject in ${TIMEOUT_SECONDS}s.`
1962
2098
  };
1963
2099
  }
1964
2100
  const command = input.tool_input.command;
1965
- const config2 = await readConfig();
1966
2101
  for (const pattern of config2.customPatterns.allow) {
1967
- try {
1968
- if (new RegExp(pattern, "i").test(command)) {
1969
- return {
1970
- decision: "allow",
1971
- reason: `Custom allow pattern: ${pattern}`,
1972
- source: "instant-allow"
1973
- };
1974
- }
1975
- } catch {
2102
+ if (safeRegexTest(pattern, command)) {
2103
+ return {
2104
+ decision: "allow",
2105
+ reason: `Custom allow pattern: ${pattern}`,
2106
+ source: "instant-allow"
2107
+ };
1976
2108
  }
1977
2109
  }
1978
2110
  for (const pattern of config2.customPatterns.block) {
1979
- try {
1980
- if (new RegExp(pattern, "i").test(command)) {
1981
- return {
1982
- decision: "needs-review",
1983
- reason: `Custom block pattern: ${pattern}`,
1984
- source: "high-risk",
1985
- userMessage: `[CUSTOM BLOCK] Matched pattern: ${pattern}
2111
+ if (safeRegexTest(pattern, command)) {
2112
+ return {
2113
+ decision: "needs-review",
2114
+ reason: `Custom block pattern: ${pattern}`,
2115
+ source: "high-risk",
2116
+ userMessage: `[CUSTOM BLOCK] Matched pattern: ${pattern}
1986
2117
 
1987
2118
  This command was blocked by your custom config.
1988
2119
 
1989
2120
  Auto-reject in ${TIMEOUT_SECONDS}s.`
1990
- };
1991
- }
1992
- } catch {
2121
+ };
1993
2122
  }
1994
2123
  }
1995
2124
  const allowResult = checkInstantAllow(command);
@@ -2027,6 +2156,14 @@ Only proceed if you know what you're doing.`
2027
2156
  if (checkpoint.type === "network") {
2028
2157
  const domainResult = checkTrustedDomains(command);
2029
2158
  if (domainResult.allTrusted && domainResult.urls.length > 0) {
2159
+ if (domainResult.hasRiskyUrls) {
2160
+ return {
2161
+ decision: "needs-review",
2162
+ reason: `Risky URL pattern from trusted domain: ${domainResult.riskyUrls.join(", ")}`,
2163
+ source: "checkpoint",
2164
+ checkpoint
2165
+ };
2166
+ }
2030
2167
  return {
2031
2168
  decision: "allow",
2032
2169
  reason: `All URLs from trusted domains: ${domainResult.trustedUrls.join(", ")}`,
@@ -2043,7 +2180,7 @@ Only proceed if you know what you're doing.`
2043
2180
  };
2044
2181
  }
2045
2182
  process.stderr.write("\x1B[90m[vibesafu] Assessing security risks...\x1B[0m\n");
2046
- const triage = await triageWithHaiku(anthropicClient, checkpoint);
2183
+ const triage = await triageWithHaiku(anthropicClient, checkpoint, config2.models.triage);
2047
2184
  if (triage.classification === "BLOCK") {
2048
2185
  return {
2049
2186
  decision: "deny",
@@ -2059,7 +2196,7 @@ Only proceed if you know what you're doing.`
2059
2196
  };
2060
2197
  }
2061
2198
  process.stderr.write("\x1B[90m[vibesafu] Escalating to deep analysis...\x1B[0m\n");
2062
- const review = await reviewWithSonnet(anthropicClient, checkpoint, triage);
2199
+ const review = await reviewWithSonnet(anthropicClient, checkpoint, triage, config2.models.review);
2063
2200
  if (review.verdict === "BLOCK") {
2064
2201
  const result2 = {
2065
2202
  decision: "deny",
@@ -2117,12 +2254,13 @@ async function runHook() {
2117
2254
  console.log(JSON.stringify(output2));
2118
2255
  return;
2119
2256
  }
2257
+ const config2 = await readConfig();
2120
2258
  let anthropicClient;
2121
- const apiKey = await getApiKey();
2259
+ const apiKey = process.env.ANTHROPIC_API_KEY ?? (config2.anthropic.apiKey || void 0);
2122
2260
  if (apiKey) {
2123
2261
  anthropicClient = new Anthropic({ apiKey });
2124
2262
  }
2125
- const result = await processPermissionRequest(input, anthropicClient);
2263
+ const result = await processPermissionRequest(input, anthropicClient, config2);
2126
2264
  let output;
2127
2265
  if (result.decision === "allow") {
2128
2266
  output = createHookOutput("allow");
@@ -2131,7 +2269,7 @@ async function runHook() {
2131
2269
  }
2132
2270
  const warningMessage = result.userMessage ?? result.reason;
2133
2271
  const timeout = result.timeoutSeconds ?? TIMEOUT_SECONDS;
2134
- await new Promise((resolve) => setTimeout(resolve, timeout * 1e3));
2272
+ await new Promise((resolve2) => setTimeout(resolve2, timeout * 1e3));
2135
2273
  const timeoutDisplay = timeout >= 3600 ? `${Math.round(timeout / 3600)}h` : `${timeout}s`;
2136
2274
  const denyMessage = `\u{1F6E1}\uFE0F [vibesafu] Auto-denied (no response in ${timeoutDisplay})
2137
2275
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibesafu",
3
- "version": "0.1.24",
3
+ "version": "0.1.26",
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",