@@ -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