Skip to content

Commit d302eef

Browse files
authored
Merge pull request #27 from kenryu42/fix/windows-path-separator
Fix/windows path separator
2 parents 562bd80 + 620ba5d commit d302eef

9 files changed

Lines changed: 308 additions & 87 deletions

File tree

.github/workflows/test-windows.yml

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
name: Windows Tests
2+
3+
on:
4+
workflow_dispatch:
5+
push:
6+
branches: ["**"]
7+
paths:
8+
- "src/**"
9+
- "tests/**"
10+
- ".github/workflows/test-windows.yml"
11+
pull_request:
12+
branches: [main]
13+
paths:
14+
- "src/**"
15+
- "tests/**"
16+
- ".github/workflows/test-windows.yml"
17+
18+
concurrency:
19+
group: ${{ github.workflow }}-${{ github.ref }}
20+
cancel-in-progress: true
21+
22+
jobs:
23+
test-windows:
24+
name: Windows Tests
25+
runs-on: windows-latest
26+
27+
steps:
28+
- name: Checkout
29+
uses: actions/checkout@v4
30+
31+
- name: Setup Bun
32+
uses: oven-sh/setup-bun@v2
33+
with:
34+
bun-version: latest
35+
36+
- name: Install dependencies
37+
run: bun install
38+
env:
39+
BUN_INSTALL_ALLOW_SCRIPTS: "@ast-grep/cli"
40+
41+
- name: Build
42+
run: bun run build
43+
44+
- name: Run tests
45+
run: bun test
46+
env:
47+
AGENT: "1"
48+
49+
- name: Test rm -rf within cwd
50+
run: |
51+
# Create test directory structure
52+
mkdir test-dir
53+
54+
# Test: rm -rf dist should be ALLOWED within cwd
55+
$result = bun run cc-safety-net explain --cwd test-dir "rm -rf dist" 2>&1
56+
Write-Host "Result: $result"
57+
58+
if ($result -match "ALLOWED") {
59+
Write-Host "✅ PASS: rm -rf dist within cwd is correctly ALLOWED"
60+
} else {
61+
Write-Host "❌ FAIL: rm -rf dist within cwd should be ALLOWED but was blocked"
62+
exit 1
63+
}
64+
shell: pwsh

.husky/pre-push

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,11 @@ bun run check
66
# Build the latest dist
77
bun run build
88

9-
# Check if dist/ changed after build
9+
# Auto-commit dist/ if it changed after build
1010
if ! git diff --quiet dist/; then
11-
echo "⚠️ dist/ is out of date! Please commit the updated dist/ folder."
11+
echo "📦 dist/ changed, auto-committing..."
12+
git add -A dist/
13+
git commit -m "chore: sync dist" --no-verify
14+
echo "⚠️ dist/ committed. Run 'git push' again to include it."
1215
exit 1
1316
fi

dist/bin/cc-safety-net.js

Lines changed: 42 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2305,7 +2305,23 @@ function analyzeGitWorktree(tokens) {
23052305
// src/core/rules-rm.ts
23062306
import { realpathSync } from "node:fs";
23072307
import { homedir as homedir3, tmpdir } from "node:os";
2308-
import { normalize, resolve as resolve2 } from "node:path";
2308+
import { normalize, resolve as resolve2, sep } from "node:path";
2309+
var IS_WINDOWS = process.platform === "win32";
2310+
function normalizePathForComparison(p) {
2311+
let normalized = normalize(p);
2312+
if (IS_WINDOWS) {
2313+
normalized = normalized.replace(/\//g, "\\");
2314+
normalized = normalized.toLowerCase();
2315+
if (normalized.length > 3 && normalized.endsWith("\\")) {
2316+
normalized = normalized.slice(0, -1);
2317+
}
2318+
} else {
2319+
if (normalized.length > 1 && normalized.endsWith("/")) {
2320+
normalized = normalized.slice(0, -1);
2321+
}
2322+
}
2323+
return normalized;
2324+
}
23092325
var REASON_RM_RF = "rm -rf outside cwd is blocked. Use explicit paths within the current directory, or delete manually.";
23102326
var REASON_RM_RF_ROOT_HOME = "rm -rf targeting root or home directory is extremely dangerous and always blocked.";
23112327
function analyzeRm(tokens, options = {}) {
@@ -2430,7 +2446,9 @@ function isTempTarget(path, allowTmpdirVar) {
24302446
return true;
24312447
}
24322448
const systemTmpdir = tmpdir();
2433-
if (normalized.startsWith(`${systemTmpdir}/`) || normalized === systemTmpdir) {
2449+
const normalizedTmpdir = normalizePathForComparison(systemTmpdir);
2450+
const pathToCompare = normalizePathForComparison(normalized);
2451+
if (pathToCompare.startsWith(`${normalizedTmpdir}${sep}`) || pathToCompare === normalizedTmpdir) {
24342452
return true;
24352453
}
24362454
if (allowTmpdirVar) {
@@ -2448,27 +2466,24 @@ function getHomeDirForRmPolicy() {
24482466
}
24492467
function isCwdHomeForRmPolicy(cwd, homeDir) {
24502468
try {
2451-
const normalizedCwd = normalize(cwd);
2452-
const normalizedHome = normalize(homeDir);
2453-
return normalizedCwd === normalizedHome;
2469+
return normalizePathForComparison(cwd) === normalizePathForComparison(homeDir);
24542470
} catch {
24552471
return false;
24562472
}
24572473
}
24582474
function isCwdSelfTarget(target, cwd) {
2459-
if (target === "." || target === "./") {
2475+
if (target === "." || target === "./" || target === ".\\") {
24602476
return true;
24612477
}
24622478
try {
24632479
const resolved = resolve2(cwd, target);
24642480
const realCwd = realpathSync(cwd);
24652481
const realResolved = realpathSync(resolved);
2466-
return realResolved === realCwd;
2482+
return normalizePathForComparison(realResolved) === normalizePathForComparison(realCwd);
24672483
} catch {
24682484
try {
24692485
const resolved = resolve2(cwd, target);
2470-
const normalizedCwd = normalize(cwd);
2471-
return resolved === normalizedCwd;
2486+
return normalizePathForComparison(resolved) === normalizePathForComparison(cwd);
24722487
} catch {
24732488
return false;
24742489
}
@@ -2482,20 +2497,21 @@ function isTargetWithinCwd(target, originalCwd, effectiveCwd) {
24822497
if (target.includes("$") || target.includes("`")) {
24832498
return false;
24842499
}
2485-
if (target.startsWith("/")) {
2500+
if (target.startsWith("/") || /^[A-Za-z]:[\\/]/.test(target)) {
24862501
try {
2487-
const normalizedTarget = normalize(target);
2488-
const normalizedCwd = `${normalize(originalCwd)}/`;
2502+
const normalizedTarget = normalizePathForComparison(target);
2503+
const normalizedCwd = `${normalizePathForComparison(originalCwd)}${sep}`;
24892504
return normalizedTarget.startsWith(normalizedCwd);
24902505
} catch {
24912506
return false;
24922507
}
24932508
}
2494-
if (target.startsWith("./") || !target.includes("/")) {
2509+
if (target.startsWith("./") || target.startsWith(".\\") || !target.includes("/") && !target.includes("\\")) {
24952510
try {
24962511
const resolved = resolve2(resolveCwd, target);
2497-
const normalizedOriginalCwd = normalize(originalCwd);
2498-
return resolved.startsWith(`${normalizedOriginalCwd}/`) || resolved === normalizedOriginalCwd;
2512+
const normalizedResolved = normalizePathForComparison(resolved);
2513+
const normalizedOriginalCwd = normalizePathForComparison(originalCwd);
2514+
return normalizedResolved.startsWith(`${normalizedOriginalCwd}${sep}`) || normalizedResolved === normalizedOriginalCwd;
24992515
} catch {
25002516
return false;
25012517
}
@@ -2505,18 +2521,17 @@ function isTargetWithinCwd(target, originalCwd, effectiveCwd) {
25052521
}
25062522
try {
25072523
const resolved = resolve2(resolveCwd, target);
2508-
const normalizedCwd = normalize(originalCwd);
2509-
return resolved.startsWith(`${normalizedCwd}/`) || resolved === normalizedCwd;
2524+
const normalizedResolved = normalizePathForComparison(resolved);
2525+
const normalizedCwd = normalizePathForComparison(originalCwd);
2526+
return normalizedResolved.startsWith(`${normalizedCwd}${sep}`) || normalizedResolved === normalizedCwd;
25102527
} catch {
25112528
return false;
25122529
}
25132530
}
25142531
function isHomeDirectory(cwd) {
25152532
const home = process.env.HOME ?? homedir3();
25162533
try {
2517-
const normalizedCwd = normalize(cwd);
2518-
const normalizedHome = normalize(home);
2519-
return normalizedCwd === normalizedHome;
2534+
return normalizePathForComparison(cwd) === normalizePathForComparison(home);
25202535
} catch {
25212536
return false;
25222537
}
@@ -3608,6 +3623,7 @@ function printReport(report) {
36083623

36093624
// src/bin/explain/config.ts
36103625
import { existsSync as existsSync5 } from "node:fs";
3626+
import { resolve as resolve3 } from "node:path";
36113627

36123628
// src/core/env.ts
36133629
function envTruthy(name) {
@@ -3637,7 +3653,7 @@ function getConfigSource(options) {
36373653
return { configSource: null, configValid: true };
36383654
}
36393655
function buildAnalyzeOptions(explainOptions) {
3640-
const cwd = explainOptions?.cwd ?? process.cwd();
3656+
const cwd = resolve3(explainOptions?.cwd ?? process.cwd());
36413657
const paranoidAll = envTruthy("SAFETY_NET_PARANOID");
36423658
return {
36433659
cwd,
@@ -4852,18 +4868,18 @@ async function readStdinAsync() {
48524868
if (process.stdin.isTTY) {
48534869
return null;
48544870
}
4855-
return new Promise((resolve3) => {
4871+
return new Promise((resolve4) => {
48564872
let data = "";
48574873
process.stdin.setEncoding("utf-8");
48584874
process.stdin.on("data", (chunk) => {
48594875
data += chunk;
48604876
});
48614877
process.stdin.on("end", () => {
48624878
const trimmed = data.trim();
4863-
resolve3(trimmed || null);
4879+
resolve4(trimmed || null);
48644880
});
48654881
process.stdin.on("error", () => {
4866-
resolve3(null);
4882+
resolve4(null);
48674883
});
48684884
});
48694885
}
@@ -4927,7 +4943,7 @@ async function printStatusline() {
49274943

49284944
// src/bin/verify-config.ts
49294945
import { existsSync as existsSync8, readFileSync as readFileSync6, writeFileSync } from "node:fs";
4930-
import { resolve as resolve3 } from "node:path";
4946+
import { resolve as resolve4 } from "node:path";
49314947
var HEADER = "Safety Net Config";
49324948
var SEPARATOR = "═".repeat(HEADER.length);
49334949
var SCHEMA_URL = "https://raw.githubusercontent.com/kenryu42/claude-code-safety-net/main/assets/cc-safety-net.schema.json";
@@ -4992,7 +5008,7 @@ function verifyConfig(options = {}) {
49925008
const result = validateConfigFile(projectConfig);
49935009
configsChecked.push({
49945010
scope: "Project",
4995-
path: resolve3(projectConfig),
5011+
path: resolve4(projectConfig),
49965012
result
49975013
});
49985014
if (result.errors.length > 0) {

dist/index.js

Lines changed: 35 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1166,7 +1166,23 @@ function analyzeGitWorktree(tokens) {
11661166
// src/core/rules-rm.ts
11671167
import { realpathSync } from "node:fs";
11681168
import { homedir, tmpdir } from "node:os";
1169-
import { normalize, resolve } from "node:path";
1169+
import { normalize, resolve, sep } from "node:path";
1170+
var IS_WINDOWS = process.platform === "win32";
1171+
function normalizePathForComparison(p) {
1172+
let normalized = normalize(p);
1173+
if (IS_WINDOWS) {
1174+
normalized = normalized.replace(/\//g, "\\");
1175+
normalized = normalized.toLowerCase();
1176+
if (normalized.length > 3 && normalized.endsWith("\\")) {
1177+
normalized = normalized.slice(0, -1);
1178+
}
1179+
} else {
1180+
if (normalized.length > 1 && normalized.endsWith("/")) {
1181+
normalized = normalized.slice(0, -1);
1182+
}
1183+
}
1184+
return normalized;
1185+
}
11701186
var REASON_RM_RF = "rm -rf outside cwd is blocked. Use explicit paths within the current directory, or delete manually.";
11711187
var REASON_RM_RF_ROOT_HOME = "rm -rf targeting root or home directory is extremely dangerous and always blocked.";
11721188
function analyzeRm(tokens, options = {}) {
@@ -1291,7 +1307,9 @@ function isTempTarget(path, allowTmpdirVar) {
12911307
return true;
12921308
}
12931309
const systemTmpdir = tmpdir();
1294-
if (normalized.startsWith(`${systemTmpdir}/`) || normalized === systemTmpdir) {
1310+
const normalizedTmpdir = normalizePathForComparison(systemTmpdir);
1311+
const pathToCompare = normalizePathForComparison(normalized);
1312+
if (pathToCompare.startsWith(`${normalizedTmpdir}${sep}`) || pathToCompare === normalizedTmpdir) {
12951313
return true;
12961314
}
12971315
if (allowTmpdirVar) {
@@ -1309,27 +1327,24 @@ function getHomeDirForRmPolicy() {
13091327
}
13101328
function isCwdHomeForRmPolicy(cwd, homeDir) {
13111329
try {
1312-
const normalizedCwd = normalize(cwd);
1313-
const normalizedHome = normalize(homeDir);
1314-
return normalizedCwd === normalizedHome;
1330+
return normalizePathForComparison(cwd) === normalizePathForComparison(homeDir);
13151331
} catch {
13161332
return false;
13171333
}
13181334
}
13191335
function isCwdSelfTarget(target, cwd) {
1320-
if (target === "." || target === "./") {
1336+
if (target === "." || target === "./" || target === ".\\") {
13211337
return true;
13221338
}
13231339
try {
13241340
const resolved = resolve(cwd, target);
13251341
const realCwd = realpathSync(cwd);
13261342
const realResolved = realpathSync(resolved);
1327-
return realResolved === realCwd;
1343+
return normalizePathForComparison(realResolved) === normalizePathForComparison(realCwd);
13281344
} catch {
13291345
try {
13301346
const resolved = resolve(cwd, target);
1331-
const normalizedCwd = normalize(cwd);
1332-
return resolved === normalizedCwd;
1347+
return normalizePathForComparison(resolved) === normalizePathForComparison(cwd);
13331348
} catch {
13341349
return false;
13351350
}
@@ -1343,20 +1358,21 @@ function isTargetWithinCwd(target, originalCwd, effectiveCwd) {
13431358
if (target.includes("$") || target.includes("`")) {
13441359
return false;
13451360
}
1346-
if (target.startsWith("/")) {
1361+
if (target.startsWith("/") || /^[A-Za-z]:[\\/]/.test(target)) {
13471362
try {
1348-
const normalizedTarget = normalize(target);
1349-
const normalizedCwd = `${normalize(originalCwd)}/`;
1363+
const normalizedTarget = normalizePathForComparison(target);
1364+
const normalizedCwd = `${normalizePathForComparison(originalCwd)}${sep}`;
13501365
return normalizedTarget.startsWith(normalizedCwd);
13511366
} catch {
13521367
return false;
13531368
}
13541369
}
1355-
if (target.startsWith("./") || !target.includes("/")) {
1370+
if (target.startsWith("./") || target.startsWith(".\\") || !target.includes("/") && !target.includes("\\")) {
13561371
try {
13571372
const resolved = resolve(resolveCwd, target);
1358-
const normalizedOriginalCwd = normalize(originalCwd);
1359-
return resolved.startsWith(`${normalizedOriginalCwd}/`) || resolved === normalizedOriginalCwd;
1373+
const normalizedResolved = normalizePathForComparison(resolved);
1374+
const normalizedOriginalCwd = normalizePathForComparison(originalCwd);
1375+
return normalizedResolved.startsWith(`${normalizedOriginalCwd}${sep}`) || normalizedResolved === normalizedOriginalCwd;
13601376
} catch {
13611377
return false;
13621378
}
@@ -1366,18 +1382,17 @@ function isTargetWithinCwd(target, originalCwd, effectiveCwd) {
13661382
}
13671383
try {
13681384
const resolved = resolve(resolveCwd, target);
1369-
const normalizedCwd = normalize(originalCwd);
1370-
return resolved.startsWith(`${normalizedCwd}/`) || resolved === normalizedCwd;
1385+
const normalizedResolved = normalizePathForComparison(resolved);
1386+
const normalizedCwd = normalizePathForComparison(originalCwd);
1387+
return normalizedResolved.startsWith(`${normalizedCwd}${sep}`) || normalizedResolved === normalizedCwd;
13711388
} catch {
13721389
return false;
13731390
}
13741391
}
13751392
function isHomeDirectory(cwd) {
13761393
const home = process.env.HOME ?? homedir();
13771394
try {
1378-
const normalizedCwd = normalize(cwd);
1379-
const normalizedHome = normalize(home);
1380-
return normalizedCwd === normalizedHome;
1395+
return normalizePathForComparison(cwd) === normalizePathForComparison(home);
13811396
} catch {
13821397
return false;
13831398
}

src/bin/explain/config.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
*/
55

66
import { existsSync } from 'node:fs';
7+
import { resolve } from 'node:path';
78
import {
89
getProjectConfigPath,
910
getUserConfigPath,
@@ -56,7 +57,8 @@ export function getConfigSource(options?: GetConfigSourceOptions): {
5657
* Merges user options with environment variable defaults.
5758
*/
5859
export function buildAnalyzeOptions(explainOptions?: ExplainOptions): AnalyzeOptions {
59-
const cwd = explainOptions?.cwd ?? process.cwd();
60+
// Resolve to absolute path - relative paths break cwd comparison logic
61+
const cwd = resolve(explainOptions?.cwd ?? process.cwd());
6062
const paranoidAll = envTruthy('SAFETY_NET_PARANOID');
6163
return {
6264
cwd,

0 commit comments

Comments
 (0)