vibesafu 0.1.24 → 0.1.25

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 +32 -14
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -84,7 +84,7 @@ async function uninstall() {
84
84
  }
85
85
 
86
86
  // src/cli/config.ts
87
- import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2 } from "fs/promises";
87
+ import { readFile as readFile2, writeFile as writeFile2, mkdir as mkdir2, chmod } from "fs/promises";
88
88
  import { homedir as homedir2 } from "os";
89
89
  import { join as join2 } from "path";
90
90
  import { createInterface } from "readline";
@@ -109,10 +109,20 @@ var DEFAULT_CONFIG = {
109
109
  path: join2(CONFIG_DIR, "logs")
110
110
  }
111
111
  };
112
+ function mergeConfig(defaults, user) {
113
+ return {
114
+ anthropic: { ...defaults.anthropic, ...user.anthropic },
115
+ models: { ...defaults.models, ...user.models },
116
+ trustedDomains: user.trustedDomains ?? defaults.trustedDomains,
117
+ customPatterns: { ...defaults.customPatterns, ...user.customPatterns },
118
+ allowedMCPTools: user.allowedMCPTools ?? defaults.allowedMCPTools,
119
+ logging: { ...defaults.logging, ...user.logging }
120
+ };
121
+ }
112
122
  async function readConfig() {
113
123
  try {
114
124
  const content = await readFile2(CONFIG_PATH, "utf-8");
115
- return { ...DEFAULT_CONFIG, ...JSON.parse(content) };
125
+ return mergeConfig(DEFAULT_CONFIG, JSON.parse(content));
116
126
  } catch {
117
127
  return DEFAULT_CONFIG;
118
128
  }
@@ -120,6 +130,7 @@ async function readConfig() {
120
130
  async function writeConfig(config2) {
121
131
  await mkdir2(CONFIG_DIR, { recursive: true });
122
132
  await writeFile2(CONFIG_PATH, JSON.stringify(config2, null, 2));
133
+ await chmod(CONFIG_PATH, 384);
123
134
  }
124
135
  function prompt(question) {
125
136
  const rl = createInterface({
@@ -811,6 +822,7 @@ var CHECKPOINT_PATTERNS = [
811
822
  { pattern: /\.ssh/i, type: "file_sensitive", description: "SSH directory access" },
812
823
  { pattern: /\.aws/i, type: "file_sensitive", description: "AWS credentials access" },
813
824
  { pattern: /credentials/i, type: "file_sensitive", description: "Credentials file access" },
825
+ { pattern: /CLAUDE\.md/i, type: "file_sensitive", description: "CLAUDE.md modification" },
814
826
  // Sensitive file copy/move (indirect path bypass)
815
827
  { pattern: /(cp|mv)\s+.*\.ssh\//i, type: "file_sensitive", description: "Copying/moving SSH files" },
816
828
  { pattern: /(cp|mv)\s+.*\.aws\//i, type: "file_sensitive", description: "Copying/moving AWS credentials" },
@@ -1280,6 +1292,13 @@ var WRITE_SENSITIVE_PATHS = [
1280
1292
  legitimateUses: ["Configuring PyPI", "Publishing packages"]
1281
1293
  },
1282
1294
  // Claude Code config - Critical (could disable security)
1295
+ {
1296
+ pattern: /CLAUDE\.md$/i,
1297
+ description: "Claude instructions file",
1298
+ severity: "critical",
1299
+ risk: "Can modify AI behavior and disable security rules",
1300
+ legitimateUses: ["Updating project instructions", "Configuring Claude behavior"]
1301
+ },
1283
1302
  {
1284
1303
  pattern: /^~?\/?\.claude\//i,
1285
1304
  description: "Claude config directory",
@@ -1535,7 +1554,7 @@ function sanitizeForPrompt(command) {
1535
1554
  if (sanitized.length > MAX_COMMAND_LENGTH) {
1536
1555
  sanitized = sanitized.slice(0, MAX_COMMAND_LENGTH) + "... [truncated]";
1537
1556
  }
1538
- sanitized = sanitized.replace(/</g, "&lt;").replace(/>/g, "&gt;");
1557
+ sanitized = sanitized.replace(/]]>/g, "]]&gt;");
1539
1558
  sanitized = sanitized.replace(/\n{3,}/g, "\n\n");
1540
1559
  return sanitized;
1541
1560
  }
@@ -1582,7 +1601,7 @@ function shouldForceEscalate(command) {
1582
1601
  }
1583
1602
 
1584
1603
  // src/guard/haiku-triage.ts
1585
- var HAIKU_MODEL = "claude-haiku-4-20250514";
1604
+ var DEFAULT_HAIKU_MODEL = "claude-haiku-4-20250514";
1586
1605
  var API_TIMEOUT_MS = 3e4;
1587
1606
  var TRIAGE_SYSTEM_PROMPT = `You are a security triage agent for an autonomous coding system.
1588
1607
  Your ONLY job is to classify commands as SELF_HANDLE, ESCALATE, or BLOCK.
@@ -1629,7 +1648,7 @@ var FORCE_ESCALATE_TYPES = [
1629
1648
  "package_install"
1630
1649
  // Supply chain attacks via postinstall scripts
1631
1650
  ];
1632
- async function triageWithHaiku(client, checkpoint) {
1651
+ async function triageWithHaiku(client, checkpoint, model) {
1633
1652
  if (FORCE_ESCALATE_TYPES.includes(checkpoint.type)) {
1634
1653
  return {
1635
1654
  classification: "ESCALATE",
@@ -1644,7 +1663,7 @@ async function triageWithHaiku(client, checkpoint) {
1644
1663
  const timeoutId = setTimeout(() => controller.abort(), API_TIMEOUT_MS);
1645
1664
  const response = await client.messages.create(
1646
1665
  {
1647
- model: HAIKU_MODEL,
1666
+ model: model ?? DEFAULT_HAIKU_MODEL,
1648
1667
  max_tokens: 500,
1649
1668
  system: TRIAGE_SYSTEM_PROMPT,
1650
1669
  messages: [{ role: "user", content: userPrompt }]
@@ -1706,7 +1725,7 @@ async function triageWithHaiku(client, checkpoint) {
1706
1725
  }
1707
1726
 
1708
1727
  // src/guard/sonnet-review.ts
1709
- var SONNET_MODEL = "claude-sonnet-4-20250514";
1728
+ var DEFAULT_SONNET_MODEL = "claude-sonnet-4-20250514";
1710
1729
  var API_TIMEOUT_MS2 = 6e4;
1711
1730
  var REVIEW_SYSTEM_PROMPT = `You are a senior security engineer reviewing potentially risky operations.
1712
1731
  Your job is to analyze commands and determine if they are safe to execute.
@@ -1778,7 +1797,7 @@ BLOCK - Do not allow:
1778
1797
  "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
1798
  }
1780
1799
  </response_format>`;
1781
- async function reviewWithSonnet(client, checkpoint, triage) {
1800
+ async function reviewWithSonnet(client, checkpoint, triage, model) {
1782
1801
  const sanitizedCommand = sanitizeForPrompt(checkpoint.command);
1783
1802
  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
1803
  try {
@@ -1786,7 +1805,7 @@ async function reviewWithSonnet(client, checkpoint, triage) {
1786
1805
  const timeoutId = setTimeout(() => controller.abort(), API_TIMEOUT_MS2);
1787
1806
  const response = await client.messages.create(
1788
1807
  {
1789
- model: SONNET_MODEL,
1808
+ model: model ?? DEFAULT_SONNET_MODEL,
1790
1809
  max_tokens: 1e3,
1791
1810
  system: REVIEW_SYSTEM_PROMPT,
1792
1811
  messages: [{ role: "user", content: userPrompt }]
@@ -1855,6 +1874,7 @@ var TIMEOUT_SECONDS = 7;
1855
1874
  var PLAN_MODE_TIMEOUT_SECONDS = 72 * 60 * 60;
1856
1875
  var SAFE_NON_BASH_TOOLS = ["WebFetch", "WebSearch", "Task", "Glob", "Grep", "LS", "TodoRead", "TodoWrite", "NotebookRead"];
1857
1876
  async function processPermissionRequest(input, anthropicClient) {
1877
+ const config2 = await readConfig();
1858
1878
  if (input.tool_name === "Write" || input.tool_name === "Edit" || input.tool_name === "Read") {
1859
1879
  const fileCheck = checkFileTool(input.tool_name, input.tool_input);
1860
1880
  if (fileCheck.blocked) {
@@ -1917,8 +1937,7 @@ This will auto-reject if not approved.`,
1917
1937
  };
1918
1938
  }
1919
1939
  if (input.tool_name.startsWith("mcp__")) {
1920
- const config3 = await readConfig();
1921
- const isAllowed = config3.allowedMCPTools.some((pattern) => {
1940
+ const isAllowed = config2.allowedMCPTools.some((pattern) => {
1922
1941
  if (pattern.endsWith("*")) {
1923
1942
  const prefix = pattern.slice(0, -1);
1924
1943
  return input.tool_name.startsWith(prefix);
@@ -1962,7 +1981,6 @@ Auto-reject in ${TIMEOUT_SECONDS}s.`
1962
1981
  };
1963
1982
  }
1964
1983
  const command = input.tool_input.command;
1965
- const config2 = await readConfig();
1966
1984
  for (const pattern of config2.customPatterns.allow) {
1967
1985
  try {
1968
1986
  if (new RegExp(pattern, "i").test(command)) {
@@ -2043,7 +2061,7 @@ Only proceed if you know what you're doing.`
2043
2061
  };
2044
2062
  }
2045
2063
  process.stderr.write("\x1B[90m[vibesafu] Assessing security risks...\x1B[0m\n");
2046
- const triage = await triageWithHaiku(anthropicClient, checkpoint);
2064
+ const triage = await triageWithHaiku(anthropicClient, checkpoint, config2.models.triage);
2047
2065
  if (triage.classification === "BLOCK") {
2048
2066
  return {
2049
2067
  decision: "deny",
@@ -2059,7 +2077,7 @@ Only proceed if you know what you're doing.`
2059
2077
  };
2060
2078
  }
2061
2079
  process.stderr.write("\x1B[90m[vibesafu] Escalating to deep analysis...\x1B[0m\n");
2062
- const review = await reviewWithSonnet(anthropicClient, checkpoint, triage);
2080
+ const review = await reviewWithSonnet(anthropicClient, checkpoint, triage, config2.models.review);
2063
2081
  if (review.verdict === "BLOCK") {
2064
2082
  const result2 = {
2065
2083
  decision: "deny",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibesafu",
3
- "version": "0.1.24",
3
+ "version": "0.1.25",
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",