Skip to content

Commit 8cadf36

Browse files
committed
fix(cli): add --allow-broad-cwd; require confirmation or flag in broad-CWD mode
1 parent fd7aade commit 8cadf36

File tree

2 files changed

+102
-10
lines changed

2 files changed

+102
-10
lines changed
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
{"created_at_ms":1775737581721,"session_id":"session-1775737581721-0","type":"session_meta","updated_at_ms":1775737581721,"version":1}
2+
{"message":{"blocks":[{"text":"hi","type":"text"}],"role":"user"},"type":"message"}

rust/crates/rusty-claude-cli/src/main.rs

Lines changed: 100 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -225,8 +225,9 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
225225
compact,
226226
base_commit,
227227
reasoning_effort,
228+
allow_broad_cwd,
228229
} => {
229-
warn_if_broad_cwd();
230+
enforce_broad_cwd_policy(allow_broad_cwd, output_format)?;
230231
run_stale_base_preflight(base_commit.as_deref());
231232
// Only consume piped stdin as prompt context when the permission
232233
// mode is fully unattended. In modes where the permission
@@ -259,12 +260,14 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
259260
permission_mode,
260261
base_commit,
261262
reasoning_effort,
263+
allow_broad_cwd,
262264
} => run_repl(
263265
model,
264266
allowed_tools,
265267
permission_mode,
266268
base_commit,
267269
reasoning_effort,
270+
allow_broad_cwd,
268271
)?,
269272
CliAction::HelpTopic(topic) => print_help_topic(topic),
270273
CliAction::Help { output_format } => print_help(output_format)?,
@@ -327,6 +330,7 @@ enum CliAction {
327330
compact: bool,
328331
base_commit: Option<String>,
329332
reasoning_effort: Option<String>,
333+
allow_broad_cwd: bool,
330334
},
331335
Login {
332336
output_format: CliOutputFormat,
@@ -354,6 +358,7 @@ enum CliAction {
354358
permission_mode: PermissionMode,
355359
base_commit: Option<String>,
356360
reasoning_effort: Option<String>,
361+
allow_broad_cwd: bool,
357362
},
358363
HelpTopic(LocalHelpTopic),
359364
// prompt-mode formatting is only supported for non-interactive runs
@@ -398,6 +403,7 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
398403
let mut compact = false;
399404
let mut base_commit: Option<String> = None;
400405
let mut reasoning_effort: Option<String> = None;
406+
let mut allow_broad_cwd = false;
401407
let mut rest: Vec<String> = Vec::new();
402408
let mut index = 0;
403409

@@ -510,6 +516,10 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
510516
reasoning_effort = Some(value.to_string());
511517
index += 1;
512518
}
519+
"--allow-broad-cwd" => {
520+
allow_broad_cwd = true;
521+
index += 1;
522+
}
513523
"-p" => {
514524
// Claw Code compat: -p "prompt" = one-shot prompt
515525
let prompt = args[index + 1..].join(" ");
@@ -526,6 +536,7 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
526536
compact,
527537
base_commit: base_commit.clone(),
528538
reasoning_effort: reasoning_effort.clone(),
539+
allow_broad_cwd,
529540
});
530541
}
531542
"--print" => {
@@ -597,6 +608,7 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
597608
compact: false,
598609
base_commit,
599610
reasoning_effort,
611+
allow_broad_cwd,
600612
});
601613
}
602614
}
@@ -606,6 +618,7 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
606618
permission_mode,
607619
base_commit,
608620
reasoning_effort: reasoning_effort.clone(),
621+
allow_broad_cwd,
609622
});
610623
}
611624
if rest.first().map(String::as_str) == Some("--resume") {
@@ -645,6 +658,7 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
645658
compact,
646659
base_commit,
647660
reasoning_effort: reasoning_effort.clone(),
661+
allow_broad_cwd,
648662
}),
649663
SkillSlashDispatch::Local => Ok(CliAction::Skills {
650664
args,
@@ -671,6 +685,7 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
671685
compact,
672686
base_commit: base_commit.clone(),
673687
reasoning_effort: reasoning_effort.clone(),
688+
allow_broad_cwd,
674689
})
675690
}
676691
other if other.starts_with('/') => parse_direct_slash_cli_action(
@@ -682,6 +697,7 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
682697
compact,
683698
base_commit,
684699
reasoning_effort,
700+
allow_broad_cwd,
685701
),
686702
_other => Ok(CliAction::Prompt {
687703
prompt: rest.join(" "),
@@ -692,6 +708,7 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
692708
compact,
693709
base_commit,
694710
reasoning_effort: reasoning_effort.clone(),
711+
allow_broad_cwd,
695712
}),
696713
}
697714
}
@@ -786,6 +803,7 @@ fn parse_direct_slash_cli_action(
786803
compact: bool,
787804
base_commit: Option<String>,
788805
reasoning_effort: Option<String>,
806+
allow_broad_cwd: bool,
789807
) -> Result<CliAction, String> {
790808
let raw = rest.join(" ");
791809
match SlashCommand::parse(&raw) {
@@ -814,6 +832,7 @@ fn parse_direct_slash_cli_action(
814832
compact,
815833
base_commit,
816834
reasoning_effort: reasoning_effort.clone(),
835+
allow_broad_cwd,
817836
}),
818837
SkillSlashDispatch::Local => Ok(CliAction::Skills {
819838
args,
@@ -2899,25 +2918,82 @@ fn run_resume_command(
28992918
}
29002919
}
29012920

2902-
/// Stale-base preflight: verify the worktree HEAD matches the expected base
2903-
/// commit (from `--base-commit` flag or `.claw-base` file). Emits a warning to
2904-
/// stderr when the HEAD has diverged.
2905-
/// Warn when the working directory is very broad (home directory or filesystem
2906-
/// root). claw scopes its file-system access to the working directory, so
2907-
/// starting from a home folder can expose/scan far more than intended.
2908-
fn warn_if_broad_cwd() {
2909-
let Ok(cwd) = env::current_dir() else { return };
2921+
/// Detect if the current working directory is "broad" (home directory or
2922+
/// filesystem root). Returns the cwd path if broad, None otherwise.
2923+
fn detect_broad_cwd() -> Option<PathBuf> {
2924+
let Ok(cwd) = env::current_dir() else {
2925+
return None;
2926+
};
29102927
let is_home = env::var_os("HOME")
29112928
.map(|h| PathBuf::from(h) == cwd)
29122929
.unwrap_or(false);
29132930
let is_root = cwd.parent().is_none();
29142931
if is_home || is_root {
2932+
Some(cwd)
2933+
} else {
2934+
None
2935+
}
2936+
}
2937+
2938+
/// Enforce the broad-CWD policy: when running from home or root, either
2939+
/// require the --allow-broad-cwd flag, or prompt for confirmation (interactive),
2940+
/// or exit with an error (non-interactive).
2941+
fn enforce_broad_cwd_policy(
2942+
allow_broad_cwd: bool,
2943+
output_format: CliOutputFormat,
2944+
) -> Result<(), Box<dyn std::error::Error>> {
2945+
if allow_broad_cwd {
2946+
return Ok(());
2947+
}
2948+
let Some(cwd) = detect_broad_cwd() else {
2949+
return Ok(());
2950+
};
2951+
2952+
let is_interactive = io::stdin().is_terminal();
2953+
2954+
if is_interactive {
2955+
// Interactive mode: print warning and ask for confirmation
29152956
eprintln!(
29162957
"Warning: claw is running from a very broad directory ({}).\n\
29172958
The agent can read and search everything under this path.\n\
29182959
Consider running from inside your project: cd /path/to/project && claw",
29192960
cwd.display()
29202961
);
2962+
eprint!("Continue anyway? [y/N]: ");
2963+
io::stderr().flush()?;
2964+
2965+
let mut input = String::new();
2966+
io::stdin().read_line(&mut input)?;
2967+
let trimmed = input.trim().to_lowercase();
2968+
if trimmed != "y" && trimmed != "yes" {
2969+
eprintln!("Aborted.");
2970+
std::process::exit(0);
2971+
}
2972+
Ok(())
2973+
} else {
2974+
// Non-interactive mode: exit with error (JSON or text)
2975+
let message = format!(
2976+
"claw is running from a very broad directory ({}). \
2977+
The agent can read and search everything under this path. \
2978+
Use --allow-broad-cwd to proceed anyway, \
2979+
or run from inside your project: cd /path/to/project && claw",
2980+
cwd.display()
2981+
);
2982+
match output_format {
2983+
CliOutputFormat::Json => {
2984+
eprintln!(
2985+
"{}",
2986+
serde_json::json!({
2987+
"type": "error",
2988+
"error": message,
2989+
})
2990+
);
2991+
}
2992+
CliOutputFormat::Text => {
2993+
eprintln!("error: {message}");
2994+
}
2995+
}
2996+
std::process::exit(1);
29212997
}
29222998
}
29232999

@@ -2939,8 +3015,9 @@ fn run_repl(
29393015
permission_mode: PermissionMode,
29403016
base_commit: Option<String>,
29413017
reasoning_effort: Option<String>,
3018+
allow_broad_cwd: bool,
29423019
) -> Result<(), Box<dyn std::error::Error>> {
2943-
warn_if_broad_cwd();
3020+
enforce_broad_cwd_policy(allow_broad_cwd, CliOutputFormat::Text)?;
29443021
run_stale_base_preflight(base_commit.as_deref());
29453022
let resolved_model = resolve_repl_model(model);
29463023
let mut cli = LiveCli::new(resolved_model, true, allowed_tools, permission_mode)?;
@@ -8461,6 +8538,7 @@ mod tests {
84618538
permission_mode: PermissionMode::DangerFullAccess,
84628539
base_commit: None,
84638540
reasoning_effort: None,
8541+
allow_broad_cwd: false,
84648542
}
84658543
);
84668544
}
@@ -8625,6 +8703,7 @@ mod tests {
86258703
compact: false,
86268704
base_commit: None,
86278705
reasoning_effort: None,
8706+
allow_broad_cwd: false,
86288707
}
86298708
);
86308709
}
@@ -8715,6 +8794,7 @@ mod tests {
87158794
compact: false,
87168795
base_commit: None,
87178796
reasoning_effort: None,
8797+
allow_broad_cwd: false,
87188798
}
87198799
);
87208800
}
@@ -8745,6 +8825,7 @@ mod tests {
87458825
compact: true,
87468826
base_commit: None,
87478827
reasoning_effort: None,
8828+
allow_broad_cwd: false,
87488829
}
87498830
);
87508831
}
@@ -8787,6 +8868,7 @@ mod tests {
87878868
compact: false,
87888869
base_commit: None,
87898870
reasoning_effort: None,
8871+
allow_broad_cwd: false,
87908872
}
87918873
);
87928874
}
@@ -8865,6 +8947,7 @@ mod tests {
88658947
permission_mode: PermissionMode::ReadOnly,
88668948
base_commit: None,
88678949
reasoning_effort: None,
8950+
allow_broad_cwd: false,
88688951
}
88698952
);
88708953
}
@@ -8885,6 +8968,7 @@ mod tests {
88858968
permission_mode: PermissionMode::DangerFullAccess,
88868969
base_commit: None,
88878970
reasoning_effort: None,
8971+
allow_broad_cwd: false,
88888972
}
88898973
);
88908974
}
@@ -8914,6 +8998,7 @@ mod tests {
89148998
compact: false,
89158999
base_commit: None,
89169000
reasoning_effort: None,
9001+
allow_broad_cwd: false,
89179002
}
89189003
);
89199004
}
@@ -8940,6 +9025,7 @@ mod tests {
89409025
permission_mode: PermissionMode::DangerFullAccess,
89419026
base_commit: None,
89429027
reasoning_effort: None,
9028+
allow_broad_cwd: false,
89439029
}
89449030
);
89459031
}
@@ -9050,6 +9136,7 @@ mod tests {
90509136
compact: false,
90519137
base_commit: None,
90529138
reasoning_effort: None,
9139+
allow_broad_cwd: false,
90539140
}
90549141
);
90559142
assert_eq!(
@@ -9434,6 +9521,7 @@ mod tests {
94349521
compact: false,
94359522
base_commit: None,
94369523
reasoning_effort: None,
9524+
allow_broad_cwd: false,
94379525
}
94389526
);
94399527
}
@@ -9501,6 +9589,7 @@ mod tests {
95019589
compact: false,
95029590
base_commit: None,
95039591
reasoning_effort: None,
9592+
allow_broad_cwd: false,
95049593
}
95059594
);
95069595
assert_eq!(
@@ -9527,6 +9616,7 @@ mod tests {
95279616
compact: false,
95289617
base_commit: None,
95299618
reasoning_effort: None,
9619+
allow_broad_cwd: false,
95309620
}
95319621
);
95329622
let error = parse_args(&["/status".to_string()])

0 commit comments

Comments
 (0)