Skip to content

Commit 33739bb

Browse files
authored
Merge pull request #42 from kenryu42/fix/rm-home-cwd-classification
fix(rules-rm): allow temp targets when cwd is home directory
2 parents e87a8bc + d157dd6 commit 33739bb

8 files changed

Lines changed: 103 additions & 98 deletions

File tree

dist/bin/cc-safety-net.js

Lines changed: 12 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -2865,6 +2865,7 @@ function normalizePathForComparison(p) {
28652865
}
28662866
var REASON_RM_RF = "rm -rf outside cwd is blocked. Use explicit paths within the current directory, or delete manually.";
28672867
var REASON_RM_RF_ROOT_HOME = "rm -rf targeting root or home directory is extremely dangerous and always blocked.";
2868+
var REASON_RM_HOME_CWD = "rm -rf in home directory is dangerous. Change to a project directory first.";
28682869
function analyzeRm(tokens, options = {}) {
28692870
const {
28702871
cwd,
@@ -2921,18 +2922,16 @@ function classifyTarget(target, ctx) {
29212922
if (isDangerousRootOrHomeTarget(target)) {
29222923
return { kind: "root_or_home_target" };
29232924
}
2924-
const anchoredCwd = ctx.anchoredCwd;
2925-
if (anchoredCwd) {
2926-
if (isCwdSelfTarget(target, anchoredCwd)) {
2927-
return { kind: "cwd_self_target" };
2928-
}
2929-
}
29302925
if (isTempTarget(target, ctx.trustTmpdirVar)) {
29312926
return { kind: "temp_target" };
29322927
}
2928+
const anchoredCwd = ctx.anchoredCwd;
29332929
if (anchoredCwd) {
29342930
if (isCwdHomeForRmPolicy(anchoredCwd, ctx.homeDir)) {
2935-
return { kind: "root_or_home_target" };
2931+
return { kind: "home_cwd_target" };
2932+
}
2933+
if (isCwdSelfTarget(target, anchoredCwd)) {
2934+
return { kind: "cwd_self_target" };
29362935
}
29372936
if (isTargetWithinCwd(target, anchoredCwd, ctx.resolvedCwd ?? anchoredCwd)) {
29382937
return { kind: "within_anchored_cwd" };
@@ -2944,10 +2943,12 @@ function reasonForClassification(classification, ctx) {
29442943
switch (classification.kind) {
29452944
case "root_or_home_target":
29462945
return REASON_RM_RF_ROOT_HOME;
2947-
case "cwd_self_target":
2948-
return REASON_RM_RF;
29492946
case "temp_target":
29502947
return null;
2948+
case "home_cwd_target":
2949+
return REASON_RM_HOME_CWD;
2950+
case "cwd_self_target":
2951+
return REASON_RM_RF;
29512952
case "within_anchored_cwd":
29522953
if (ctx.paranoid) {
29532954
return `${REASON_RM_RF} (SAFETY_NET_PARANOID_RM enabled)`;
@@ -3069,14 +3070,6 @@ function isTargetWithinCwd(target, originalCwd, effectiveCwd) {
30693070
return false;
30703071
}
30713072
}
3072-
function isHomeDirectory(cwd) {
3073-
const home = process.env.HOME ?? homedir3();
3074-
try {
3075-
return normalizePathForComparison(cwd) === normalizePathForComparison(home);
3076-
} catch {
3077-
return false;
3078-
}
3079-
}
30803073

30813074
// src/core/analyze/parallel.ts
30823075
var REASON_PARALLEL_RM = "parallel rm -rf with dynamic input is dangerous. Use explicit file list instead.";
@@ -3497,7 +3490,6 @@ function matchesBlockArgs(tokens, blockArgs, shortOpts) {
34973490
// src/core/analyze/segment.ts
34983491
var REASON_INTERPRETER_DANGEROUS = "Detected potentially dangerous command in interpreter code.";
34993492
var REASON_INTERPRETER_BLOCKED = "Interpreter one-liners are blocked in paranoid mode.";
3500-
var REASON_RM_HOME_CWD = "rm -rf in home directory is dangerous. Change to a project directory first.";
35013493
function deriveCwdContext(options) {
35023494
const cwdUnknown = options.effectiveCwd === null;
35033495
const cwdForRm = cwdUnknown ? undefined : options.effectiveCwd ?? options.cwd;
@@ -3561,11 +3553,6 @@ function analyzeSegment(tokens, depth, options) {
35613553
}
35623554
}
35633555
if (isRm) {
3564-
if (cwdForRm && isHomeDirectory(cwdForRm)) {
3565-
if (hasRecursiveForceFlags(stripped)) {
3566-
return REASON_RM_HOME_CWD;
3567-
}
3568-
}
35693556
const rmResult = analyzeRm(stripped, {
35703557
cwd: cwdForRm,
35713558
originalCwd,
@@ -4682,19 +4669,9 @@ function explainSegment(tokens, depth, options, steps) {
46824669
return { reason };
46834670
}
46844671
if (isRm) {
4685-
if (effectiveCwd && isHomeDirectory(effectiveCwd) && hasRecursiveForceFlags(strippedTokens)) {
4686-
const reason2 = "rm -rf in home directory is dangerous. Change to a project directory first.";
4687-
steps.push({
4688-
type: "rule-check",
4689-
ruleModule: "rules-rm.ts",
4690-
ruleFunction: "isHomeDirectory",
4691-
matched: true,
4692-
reason: reason2
4693-
});
4694-
return { reason: reason2 };
4695-
}
46964672
const reason = analyzeRm(strippedTokens, {
4697-
cwd: effectiveCwd ?? undefined,
4673+
cwd: cwdForRm,
4674+
originalCwd,
46984675
paranoid: options.paranoidRm,
46994676
allowTmpdirVar
47004677
});

dist/core/rules-rm.d.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,4 +6,5 @@ export interface AnalyzeRmOptions {
66
tmpdirOverridden?: boolean;
77
}
88
export declare function analyzeRm(tokens: string[], options?: AnalyzeRmOptions): string | null;
9+
/** @internal Exported for testing */
910
export declare function isHomeDirectory(cwd: string): boolean;

dist/index.js

Lines changed: 10 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1724,6 +1724,7 @@ function normalizePathForComparison(p) {
17241724
}
17251725
var REASON_RM_RF = "rm -rf outside cwd is blocked. Use explicit paths within the current directory, or delete manually.";
17261726
var REASON_RM_RF_ROOT_HOME = "rm -rf targeting root or home directory is extremely dangerous and always blocked.";
1727+
var REASON_RM_HOME_CWD = "rm -rf in home directory is dangerous. Change to a project directory first.";
17271728
function analyzeRm(tokens, options = {}) {
17281729
const {
17291730
cwd,
@@ -1780,18 +1781,16 @@ function classifyTarget(target, ctx) {
17801781
if (isDangerousRootOrHomeTarget(target)) {
17811782
return { kind: "root_or_home_target" };
17821783
}
1783-
const anchoredCwd = ctx.anchoredCwd;
1784-
if (anchoredCwd) {
1785-
if (isCwdSelfTarget(target, anchoredCwd)) {
1786-
return { kind: "cwd_self_target" };
1787-
}
1788-
}
17891784
if (isTempTarget(target, ctx.trustTmpdirVar)) {
17901785
return { kind: "temp_target" };
17911786
}
1787+
const anchoredCwd = ctx.anchoredCwd;
17921788
if (anchoredCwd) {
17931789
if (isCwdHomeForRmPolicy(anchoredCwd, ctx.homeDir)) {
1794-
return { kind: "root_or_home_target" };
1790+
return { kind: "home_cwd_target" };
1791+
}
1792+
if (isCwdSelfTarget(target, anchoredCwd)) {
1793+
return { kind: "cwd_self_target" };
17951794
}
17961795
if (isTargetWithinCwd(target, anchoredCwd, ctx.resolvedCwd ?? anchoredCwd)) {
17971796
return { kind: "within_anchored_cwd" };
@@ -1803,10 +1802,12 @@ function reasonForClassification(classification, ctx) {
18031802
switch (classification.kind) {
18041803
case "root_or_home_target":
18051804
return REASON_RM_RF_ROOT_HOME;
1806-
case "cwd_self_target":
1807-
return REASON_RM_RF;
18081805
case "temp_target":
18091806
return null;
1807+
case "home_cwd_target":
1808+
return REASON_RM_HOME_CWD;
1809+
case "cwd_self_target":
1810+
return REASON_RM_RF;
18101811
case "within_anchored_cwd":
18111812
if (ctx.paranoid) {
18121813
return `${REASON_RM_RF} (SAFETY_NET_PARANOID_RM enabled)`;
@@ -1928,14 +1929,6 @@ function isTargetWithinCwd(target, originalCwd, effectiveCwd) {
19281929
return false;
19291930
}
19301931
}
1931-
function isHomeDirectory(cwd) {
1932-
const home = process.env.HOME ?? homedir();
1933-
try {
1934-
return normalizePathForComparison(cwd) === normalizePathForComparison(home);
1935-
} catch {
1936-
return false;
1937-
}
1938-
}
19391932

19401933
// src/core/analyze/parallel.ts
19411934
var REASON_PARALLEL_RM = "parallel rm -rf with dynamic input is dangerous. Use explicit file list instead.";
@@ -2356,7 +2349,6 @@ function matchesBlockArgs(tokens, blockArgs, shortOpts) {
23562349
// src/core/analyze/segment.ts
23572350
var REASON_INTERPRETER_DANGEROUS = "Detected potentially dangerous command in interpreter code.";
23582351
var REASON_INTERPRETER_BLOCKED = "Interpreter one-liners are blocked in paranoid mode.";
2359-
var REASON_RM_HOME_CWD = "rm -rf in home directory is dangerous. Change to a project directory first.";
23602352
function deriveCwdContext(options) {
23612353
const cwdUnknown = options.effectiveCwd === null;
23622354
const cwdForRm = cwdUnknown ? undefined : options.effectiveCwd ?? options.cwd;
@@ -2420,11 +2412,6 @@ function analyzeSegment(tokens, depth, options) {
24202412
}
24212413
}
24222414
if (isRm) {
2423-
if (cwdForRm && isHomeDirectory(cwdForRm)) {
2424-
if (hasRecursiveForceFlags(stripped)) {
2425-
return REASON_RM_HOME_CWD;
2426-
}
2427-
}
24282415
const rmResult = analyzeRm(stripped, {
24292416
cwd: cwdForRm,
24302417
originalCwd,

src/bin/explain/segment.ts

Lines changed: 3 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import { dangerousInText } from '@/core/analyze/dangerous-text';
1414
import { analyzeFind } from '@/core/analyze/find';
1515
import { containsDangerousCode, extractInterpreterCodeArg } from '@/core/analyze/interpreters';
1616
import { analyzeParallel } from '@/core/analyze/parallel';
17-
import { hasRecursiveForceFlags } from '@/core/analyze/rm-flags';
1817
import {
1918
REASON_INTERPRETER_BLOCKED,
2019
REASON_INTERPRETER_DANGEROUS,
@@ -25,7 +24,7 @@ import { isTmpdirOverriddenToNonTemp } from '@/core/analyze/tmpdir';
2524
import { analyzeXargs } from '@/core/analyze/xargs';
2625
import { checkCustomRules } from '@/core/rules-custom';
2726
import { analyzeGit } from '@/core/rules-git';
28-
import { analyzeRm, isHomeDirectory } from '@/core/rules-rm';
27+
import { analyzeRm } from '@/core/rules-rm';
2928
import {
3029
normalizeCommandToken,
3130
splitShellCommands,
@@ -302,19 +301,9 @@ export function explainSegment(
302301
}
303302

304303
if (isRm) {
305-
if (effectiveCwd && isHomeDirectory(effectiveCwd) && hasRecursiveForceFlags(strippedTokens)) {
306-
const reason = 'rm -rf in home directory is dangerous. Change to a project directory first.';
307-
steps.push({
308-
type: 'rule-check',
309-
ruleModule: 'rules-rm.ts',
310-
ruleFunction: 'isHomeDirectory',
311-
matched: true,
312-
reason,
313-
});
314-
return { reason };
315-
}
316304
const reason = analyzeRm(strippedTokens, {
317-
cwd: effectiveCwd ?? undefined,
305+
cwd: cwdForRm,
306+
originalCwd,
318307
paranoid: options.paranoidRm,
319308
allowTmpdirVar,
320309
});

src/core/analyze/segment.ts

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,12 @@ import { DISPLAY_COMMANDS } from '@/core/analyze/constants';
22
import { analyzeFind } from '@/core/analyze/find';
33
import { containsDangerousCode, extractInterpreterCodeArg } from '@/core/analyze/interpreters';
44
import { analyzeParallel } from '@/core/analyze/parallel';
5-
import { hasRecursiveForceFlags } from '@/core/analyze/rm-flags';
65
import { extractDashCArg } from '@/core/analyze/shell-wrappers';
76
import { isTmpdirOverriddenToNonTemp } from '@/core/analyze/tmpdir';
87
import { analyzeXargs } from '@/core/analyze/xargs';
98
import { checkCustomRules } from '@/core/rules-custom';
109
import { analyzeGit } from '@/core/rules-git';
11-
import { analyzeRm, isHomeDirectory } from '@/core/rules-rm';
10+
import { analyzeRm } from '@/core/rules-rm';
1211
import {
1312
getBasename,
1413
normalizeCommandToken,
@@ -27,8 +26,6 @@ import {
2726
export const REASON_INTERPRETER_DANGEROUS =
2827
'Detected potentially dangerous command in interpreter code.';
2928
export const REASON_INTERPRETER_BLOCKED = 'Interpreter one-liners are blocked in paranoid mode.';
30-
const REASON_RM_HOME_CWD =
31-
'rm -rf in home directory is dangerous. Change to a project directory first.';
3229

3330
export type InternalOptions = AnalyzeOptions & {
3431
config: Config;
@@ -123,11 +120,6 @@ export function analyzeSegment(
123120
}
124121

125122
if (isRm) {
126-
if (cwdForRm && isHomeDirectory(cwdForRm)) {
127-
if (hasRecursiveForceFlags(stripped)) {
128-
return REASON_RM_HOME_CWD;
129-
}
130-
}
131123
const rmResult = analyzeRm(stripped, {
132124
cwd: cwdForRm,
133125
originalCwd,

src/core/rules-rm.ts

Lines changed: 15 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,8 @@ const REASON_RM_RF =
3636
'rm -rf outside cwd is blocked. Use explicit paths within the current directory, or delete manually.';
3737
const REASON_RM_RF_ROOT_HOME =
3838
'rm -rf targeting root or home directory is extremely dangerous and always blocked.';
39+
const REASON_RM_HOME_CWD =
40+
'rm -rf in home directory is dangerous. Change to a project directory first.';
3941

4042
export interface AnalyzeRmOptions {
4143
cwd?: string;
@@ -55,8 +57,9 @@ interface RmContext {
5557

5658
type TargetClassification =
5759
| { kind: 'root_or_home_target' }
58-
| { kind: 'cwd_self_target' }
5960
| { kind: 'temp_target' }
61+
| { kind: 'home_cwd_target' }
62+
| { kind: 'cwd_self_target' }
6063
| { kind: 'within_anchored_cwd' }
6164
| { kind: 'outside_anchored_cwd' };
6265

@@ -127,20 +130,18 @@ function classifyTarget(target: string, ctx: RmContext): TargetClassification {
127130
return { kind: 'root_or_home_target' };
128131
}
129132

130-
const anchoredCwd = ctx.anchoredCwd;
131-
if (anchoredCwd) {
132-
if (isCwdSelfTarget(target, anchoredCwd)) {
133-
return { kind: 'cwd_self_target' };
134-
}
135-
}
136-
137133
if (isTempTarget(target, ctx.trustTmpdirVar)) {
138134
return { kind: 'temp_target' };
139135
}
140136

137+
const anchoredCwd = ctx.anchoredCwd;
141138
if (anchoredCwd) {
142139
if (isCwdHomeForRmPolicy(anchoredCwd, ctx.homeDir)) {
143-
return { kind: 'root_or_home_target' };
140+
return { kind: 'home_cwd_target' };
141+
}
142+
143+
if (isCwdSelfTarget(target, anchoredCwd)) {
144+
return { kind: 'cwd_self_target' };
144145
}
145146

146147
if (isTargetWithinCwd(target, anchoredCwd, ctx.resolvedCwd ?? anchoredCwd)) {
@@ -158,10 +159,12 @@ function reasonForClassification(
158159
switch (classification.kind) {
159160
case 'root_or_home_target':
160161
return REASON_RM_RF_ROOT_HOME;
161-
case 'cwd_self_target':
162-
return REASON_RM_RF;
163162
case 'temp_target':
164163
return null;
164+
case 'home_cwd_target':
165+
return REASON_RM_HOME_CWD;
166+
case 'cwd_self_target':
167+
return REASON_RM_RF;
165168
case 'within_anchored_cwd':
166169
if (ctx.paranoid) {
167170
return `${REASON_RM_RF} (SAFETY_NET_PARANOID_RM enabled)`;
@@ -319,6 +322,7 @@ function isTargetWithinCwd(target: string, originalCwd: string, effectiveCwd?: s
319322
}
320323
}
321324

325+
/** @internal Exported for testing */
322326
export function isHomeDirectory(cwd: string): boolean {
323327
const home = process.env.HOME ?? homedir();
324328
try {

tests/bin/explain/command.test.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -282,12 +282,23 @@ describe('explainCommand rm with home directory', () => {
282282
const allSteps = result.trace.segments.flatMap((s) => s.steps);
283283
const ruleStep = allSteps.find(
284284
(s) =>
285-
s.type === 'rule-check' &&
286-
s.ruleModule === 'rules-rm.ts' &&
287-
s.ruleFunction === 'isHomeDirectory',
285+
s.type === 'rule-check' && s.ruleModule === 'rules-rm.ts' && s.ruleFunction === 'analyzeRm',
288286
);
289287
expect(ruleStep).toBeDefined();
290288
});
289+
290+
test('temp-target rm in home directory cwd is allowed', () => {
291+
const homeDir = process.env.HOME;
292+
if (!homeDir) return;
293+
const result = explainCommand('rm -rf /tmp/test-dir', { cwd: homeDir });
294+
expect(result.result).toBe('allowed');
295+
const allSteps = result.trace.segments.flatMap((s) => s.steps);
296+
const analyzeRmStep = allSteps.find(
297+
(s) =>
298+
s.type === 'rule-check' && s.ruleModule === 'rules-rm.ts' && s.ruleFunction === 'analyzeRm',
299+
);
300+
expect(analyzeRmStep).toBeDefined();
301+
});
291302
});
292303

293304
describe('explainCommand parallel with nested blocked', () => {

0 commit comments

Comments
 (0)