vibesafu 0.1.25 → 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.
- package/dist/index.js +179 -59
- 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, chmod } 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";
|
|
@@ -123,24 +129,30 @@ async function readConfig() {
|
|
|
123
129
|
try {
|
|
124
130
|
const content = await readFile2(CONFIG_PATH, "utf-8");
|
|
125
131
|
return mergeConfig(DEFAULT_CONFIG, JSON.parse(content));
|
|
126
|
-
} catch {
|
|
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
|
+
`);
|
|
127
139
|
return DEFAULT_CONFIG;
|
|
128
140
|
}
|
|
129
141
|
}
|
|
130
142
|
async function writeConfig(config2) {
|
|
131
143
|
await mkdir2(CONFIG_DIR, { recursive: true });
|
|
132
144
|
await writeFile2(CONFIG_PATH, JSON.stringify(config2, null, 2));
|
|
133
|
-
await
|
|
145
|
+
await chmod2(CONFIG_PATH, 384);
|
|
134
146
|
}
|
|
135
147
|
function prompt(question) {
|
|
136
148
|
const rl = createInterface({
|
|
137
149
|
input: process.stdin,
|
|
138
150
|
output: process.stdout
|
|
139
151
|
});
|
|
140
|
-
return new Promise((
|
|
152
|
+
return new Promise((resolve2) => {
|
|
141
153
|
rl.question(question, (answer) => {
|
|
142
154
|
rl.close();
|
|
143
|
-
|
|
155
|
+
resolve2(answer);
|
|
144
156
|
});
|
|
145
157
|
});
|
|
146
158
|
}
|
|
@@ -166,16 +178,6 @@ async function config() {
|
|
|
166
178
|
console.log("Configuration saved!");
|
|
167
179
|
console.log(`Config file: ${CONFIG_PATH}`);
|
|
168
180
|
}
|
|
169
|
-
async function getApiKey() {
|
|
170
|
-
if (process.env.ANTHROPIC_API_KEY) {
|
|
171
|
-
return process.env.ANTHROPIC_API_KEY;
|
|
172
|
-
}
|
|
173
|
-
const cfg = await readConfig();
|
|
174
|
-
if (cfg.anthropic.apiKey) {
|
|
175
|
-
return cfg.anthropic.apiKey;
|
|
176
|
-
}
|
|
177
|
-
return void 0;
|
|
178
|
-
}
|
|
179
181
|
|
|
180
182
|
// src/hook.ts
|
|
181
183
|
import Anthropic from "@anthropic-ai/sdk";
|
|
@@ -779,6 +781,13 @@ var INSTANT_BLOCK_PATTERNS = [
|
|
|
779
781
|
...DESTRUCTIVE_PATTERNS,
|
|
780
782
|
...SELF_PROTECTION_PATTERNS
|
|
781
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
|
+
}
|
|
782
791
|
var CHECKPOINT_PATTERNS = [
|
|
783
792
|
// Script execution
|
|
784
793
|
{ pattern: /curl\s+.*\|\s*(ba)?sh/i, type: "script_execution", description: "curl piped to shell" },
|
|
@@ -822,12 +831,18 @@ var CHECKPOINT_PATTERNS = [
|
|
|
822
831
|
{ pattern: /\.ssh/i, type: "file_sensitive", description: "SSH directory access" },
|
|
823
832
|
{ pattern: /\.aws/i, type: "file_sensitive", description: "AWS credentials access" },
|
|
824
833
|
{ pattern: /credentials/i, type: "file_sensitive", description: "Credentials file access" },
|
|
825
|
-
{ pattern: /CLAUDE\.md/i, type: "file_sensitive", description: "CLAUDE.md modification" },
|
|
826
834
|
// Sensitive file copy/move (indirect path bypass)
|
|
827
835
|
{ pattern: /(cp|mv)\s+.*\.ssh\//i, type: "file_sensitive", description: "Copying/moving SSH files" },
|
|
828
836
|
{ pattern: /(cp|mv)\s+.*\.aws\//i, type: "file_sensitive", description: "Copying/moving AWS credentials" },
|
|
829
837
|
{ pattern: /(cp|mv)\s+.*\.env(\s|$)/i, type: "file_sensitive", description: "Copying/moving .env file" }
|
|
830
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
|
+
}
|
|
831
846
|
|
|
832
847
|
// src/guard/instant-block.ts
|
|
833
848
|
function checkHighRiskPatterns(command) {
|
|
@@ -1034,7 +1049,10 @@ function isTrustedUrl(url) {
|
|
|
1034
1049
|
function extractUrls(command) {
|
|
1035
1050
|
const urlPattern = /https?:\/\/[^\s"'<>]+/gi;
|
|
1036
1051
|
const matches = command.match(urlPattern);
|
|
1037
|
-
|
|
1052
|
+
if (!matches) return [];
|
|
1053
|
+
return matches.map((url) => {
|
|
1054
|
+
return url.replace(/[),;.]+$/, "");
|
|
1055
|
+
});
|
|
1038
1056
|
}
|
|
1039
1057
|
function isUrlShortener(hostname) {
|
|
1040
1058
|
const normalizedHost = hostname.toLowerCase();
|
|
@@ -1058,6 +1076,21 @@ function containsUrlShortener(command) {
|
|
|
1058
1076
|
shortenerUrls
|
|
1059
1077
|
};
|
|
1060
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
|
+
}
|
|
1061
1094
|
|
|
1062
1095
|
// src/guard/checkpoint.ts
|
|
1063
1096
|
function detectCheckpoint(command) {
|
|
@@ -1091,29 +1124,39 @@ function checkTrustedDomains(command) {
|
|
|
1091
1124
|
return {
|
|
1092
1125
|
allTrusted: true,
|
|
1093
1126
|
// No URLs means nothing untrusted
|
|
1127
|
+
hasRiskyUrls: false,
|
|
1094
1128
|
urls: [],
|
|
1095
1129
|
trustedUrls: [],
|
|
1096
|
-
untrustedUrls: []
|
|
1130
|
+
untrustedUrls: [],
|
|
1131
|
+
riskyUrls: []
|
|
1097
1132
|
};
|
|
1098
1133
|
}
|
|
1099
1134
|
const trustedUrls = [];
|
|
1100
1135
|
const untrustedUrls = [];
|
|
1136
|
+
const riskyUrls = [];
|
|
1101
1137
|
for (const url of urls) {
|
|
1102
1138
|
if (isTrustedUrl(url)) {
|
|
1103
1139
|
trustedUrls.push(url);
|
|
1104
1140
|
} else {
|
|
1105
1141
|
untrustedUrls.push(url);
|
|
1106
1142
|
}
|
|
1143
|
+
if (isRiskyUrlPattern(url)) {
|
|
1144
|
+
riskyUrls.push(url);
|
|
1145
|
+
}
|
|
1107
1146
|
}
|
|
1108
1147
|
return {
|
|
1109
1148
|
allTrusted: untrustedUrls.length === 0,
|
|
1149
|
+
hasRiskyUrls: riskyUrls.length > 0,
|
|
1110
1150
|
urls,
|
|
1111
1151
|
trustedUrls,
|
|
1112
|
-
untrustedUrls
|
|
1152
|
+
untrustedUrls,
|
|
1153
|
+
riskyUrls
|
|
1113
1154
|
};
|
|
1114
1155
|
}
|
|
1115
1156
|
|
|
1116
1157
|
// src/guard/file-tools.ts
|
|
1158
|
+
import { resolve } from "path";
|
|
1159
|
+
import { homedir as homedir3 } from "os";
|
|
1117
1160
|
var WRITE_SENSITIVE_PATHS = [
|
|
1118
1161
|
// SSH - Critical (persistent access)
|
|
1119
1162
|
{
|
|
@@ -1292,13 +1335,6 @@ var WRITE_SENSITIVE_PATHS = [
|
|
|
1292
1335
|
legitimateUses: ["Configuring PyPI", "Publishing packages"]
|
|
1293
1336
|
},
|
|
1294
1337
|
// 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
|
-
},
|
|
1302
1338
|
{
|
|
1303
1339
|
pattern: /^~?\/?\.claude\//i,
|
|
1304
1340
|
description: "Claude config directory",
|
|
@@ -1450,8 +1486,18 @@ var READ_SENSITIVE_PATHS = [
|
|
|
1450
1486
|
}
|
|
1451
1487
|
];
|
|
1452
1488
|
function normalizePath(filePath) {
|
|
1453
|
-
let normalized = filePath
|
|
1489
|
+
let normalized = filePath;
|
|
1490
|
+
normalized = normalized.replace(/\$HOME/g, "~").replace(/\$\{HOME\}/g, "~");
|
|
1454
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
|
+
}
|
|
1455
1501
|
return normalized;
|
|
1456
1502
|
}
|
|
1457
1503
|
function checkFilePath(filePath, action) {
|
|
@@ -1599,6 +1645,62 @@ function shouldForceEscalate(command) {
|
|
|
1599
1645
|
}
|
|
1600
1646
|
return FORCE_ESCALATE_PATTERNS.some((pattern) => pattern.test(command));
|
|
1601
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
|
+
}
|
|
1602
1704
|
|
|
1603
1705
|
// src/guard/haiku-triage.ts
|
|
1604
1706
|
var DEFAULT_HAIKU_MODEL = "claude-haiku-4-20250514";
|
|
@@ -1679,15 +1781,15 @@ async function triageWithHaiku(client, checkpoint, model) {
|
|
|
1679
1781
|
riskIndicators: ["triage_error"]
|
|
1680
1782
|
};
|
|
1681
1783
|
}
|
|
1682
|
-
const
|
|
1683
|
-
if (!
|
|
1784
|
+
const extracted = extractJsonFromText(text);
|
|
1785
|
+
if (!extracted) {
|
|
1684
1786
|
return {
|
|
1685
1787
|
classification: "ESCALATE",
|
|
1686
1788
|
reason: "Triage failed: Could not parse JSON response",
|
|
1687
1789
|
riskIndicators: ["triage_error"]
|
|
1688
1790
|
};
|
|
1689
1791
|
}
|
|
1690
|
-
const parsed =
|
|
1792
|
+
const parsed = extracted;
|
|
1691
1793
|
if (!parsed.classification || !["SELF_HANDLE", "ESCALATE", "BLOCK"].includes(parsed.classification)) {
|
|
1692
1794
|
return {
|
|
1693
1795
|
classification: "ESCALATE",
|
|
@@ -1822,8 +1924,8 @@ async function reviewWithSonnet(client, checkpoint, triage, model) {
|
|
|
1822
1924
|
userMessage: "Automated security review failed. Please review this operation manually."
|
|
1823
1925
|
};
|
|
1824
1926
|
}
|
|
1825
|
-
const
|
|
1826
|
-
if (!
|
|
1927
|
+
const extracted = extractJsonFromText(text);
|
|
1928
|
+
if (!extracted) {
|
|
1827
1929
|
return {
|
|
1828
1930
|
verdict: "ASK_USER",
|
|
1829
1931
|
riskLevel: "medium",
|
|
@@ -1831,7 +1933,7 @@ async function reviewWithSonnet(client, checkpoint, triage, model) {
|
|
|
1831
1933
|
userMessage: "Automated security review failed. Please review this operation manually."
|
|
1832
1934
|
};
|
|
1833
1935
|
}
|
|
1834
|
-
const parsed =
|
|
1936
|
+
const parsed = extracted;
|
|
1835
1937
|
const verdict = parsed.verdict ?? "ASK_USER";
|
|
1836
1938
|
if (!["ALLOW", "ASK_USER", "BLOCK"].includes(verdict)) {
|
|
1837
1939
|
return {
|
|
@@ -1871,10 +1973,25 @@ async function reviewWithSonnet(client, checkpoint, triage, model) {
|
|
|
1871
1973
|
|
|
1872
1974
|
// src/hook.ts
|
|
1873
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
|
+
}
|
|
1874
1991
|
var PLAN_MODE_TIMEOUT_SECONDS = 72 * 60 * 60;
|
|
1875
1992
|
var SAFE_NON_BASH_TOOLS = ["WebFetch", "WebSearch", "Task", "Glob", "Grep", "LS", "TodoRead", "TodoWrite", "NotebookRead"];
|
|
1876
|
-
async function processPermissionRequest(input, anthropicClient) {
|
|
1877
|
-
const config2 = await readConfig();
|
|
1993
|
+
async function processPermissionRequest(input, anthropicClient, preloadedConfig) {
|
|
1994
|
+
const config2 = preloadedConfig ?? await readConfig();
|
|
1878
1995
|
if (input.tool_name === "Write" || input.tool_name === "Edit" || input.tool_name === "Read") {
|
|
1879
1996
|
const fileCheck = checkFileTool(input.tool_name, input.tool_input);
|
|
1880
1997
|
if (fileCheck.blocked) {
|
|
@@ -1982,32 +2099,26 @@ Auto-reject in ${TIMEOUT_SECONDS}s.`
|
|
|
1982
2099
|
}
|
|
1983
2100
|
const command = input.tool_input.command;
|
|
1984
2101
|
for (const pattern of config2.customPatterns.allow) {
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
};
|
|
1992
|
-
}
|
|
1993
|
-
} catch {
|
|
2102
|
+
if (safeRegexTest(pattern, command)) {
|
|
2103
|
+
return {
|
|
2104
|
+
decision: "allow",
|
|
2105
|
+
reason: `Custom allow pattern: ${pattern}`,
|
|
2106
|
+
source: "instant-allow"
|
|
2107
|
+
};
|
|
1994
2108
|
}
|
|
1995
2109
|
}
|
|
1996
2110
|
for (const pattern of config2.customPatterns.block) {
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
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}
|
|
2004
2117
|
|
|
2005
2118
|
This command was blocked by your custom config.
|
|
2006
2119
|
|
|
2007
2120
|
Auto-reject in ${TIMEOUT_SECONDS}s.`
|
|
2008
|
-
|
|
2009
|
-
}
|
|
2010
|
-
} catch {
|
|
2121
|
+
};
|
|
2011
2122
|
}
|
|
2012
2123
|
}
|
|
2013
2124
|
const allowResult = checkInstantAllow(command);
|
|
@@ -2045,6 +2156,14 @@ Only proceed if you know what you're doing.`
|
|
|
2045
2156
|
if (checkpoint.type === "network") {
|
|
2046
2157
|
const domainResult = checkTrustedDomains(command);
|
|
2047
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
|
+
}
|
|
2048
2167
|
return {
|
|
2049
2168
|
decision: "allow",
|
|
2050
2169
|
reason: `All URLs from trusted domains: ${domainResult.trustedUrls.join(", ")}`,
|
|
@@ -2135,12 +2254,13 @@ async function runHook() {
|
|
|
2135
2254
|
console.log(JSON.stringify(output2));
|
|
2136
2255
|
return;
|
|
2137
2256
|
}
|
|
2257
|
+
const config2 = await readConfig();
|
|
2138
2258
|
let anthropicClient;
|
|
2139
|
-
const apiKey =
|
|
2259
|
+
const apiKey = process.env.ANTHROPIC_API_KEY ?? (config2.anthropic.apiKey || void 0);
|
|
2140
2260
|
if (apiKey) {
|
|
2141
2261
|
anthropicClient = new Anthropic({ apiKey });
|
|
2142
2262
|
}
|
|
2143
|
-
const result = await processPermissionRequest(input, anthropicClient);
|
|
2263
|
+
const result = await processPermissionRequest(input, anthropicClient, config2);
|
|
2144
2264
|
let output;
|
|
2145
2265
|
if (result.decision === "allow") {
|
|
2146
2266
|
output = createHookOutput("allow");
|
|
@@ -2149,7 +2269,7 @@ async function runHook() {
|
|
|
2149
2269
|
}
|
|
2150
2270
|
const warningMessage = result.userMessage ?? result.reason;
|
|
2151
2271
|
const timeout = result.timeoutSeconds ?? TIMEOUT_SECONDS;
|
|
2152
|
-
await new Promise((
|
|
2272
|
+
await new Promise((resolve2) => setTimeout(resolve2, timeout * 1e3));
|
|
2153
2273
|
const timeoutDisplay = timeout >= 3600 ? `${Math.round(timeout / 3600)}h` : `${timeout}s`;
|
|
2154
2274
|
const denyMessage = `\u{1F6E1}\uFE0F [vibesafu] Auto-denied (no response in ${timeoutDisplay})
|
|
2155
2275
|
|
package/package.json
CHANGED