mirror of
https://github.com/instructkr/claw-code.git
synced 2026-04-10 10:04:50 +08:00
fix(cli): add --allow-broad-cwd; require confirmation or flag in broad-CWD mode
This commit is contained in:
@@ -225,8 +225,9 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
compact,
|
compact,
|
||||||
base_commit,
|
base_commit,
|
||||||
reasoning_effort,
|
reasoning_effort,
|
||||||
|
allow_broad_cwd,
|
||||||
} => {
|
} => {
|
||||||
warn_if_broad_cwd();
|
enforce_broad_cwd_policy(allow_broad_cwd, output_format)?;
|
||||||
run_stale_base_preflight(base_commit.as_deref());
|
run_stale_base_preflight(base_commit.as_deref());
|
||||||
// Only consume piped stdin as prompt context when the permission
|
// Only consume piped stdin as prompt context when the permission
|
||||||
// mode is fully unattended. In modes where the permission
|
// mode is fully unattended. In modes where the permission
|
||||||
@@ -259,12 +260,14 @@ fn run() -> Result<(), Box<dyn std::error::Error>> {
|
|||||||
permission_mode,
|
permission_mode,
|
||||||
base_commit,
|
base_commit,
|
||||||
reasoning_effort,
|
reasoning_effort,
|
||||||
|
allow_broad_cwd,
|
||||||
} => run_repl(
|
} => run_repl(
|
||||||
model,
|
model,
|
||||||
allowed_tools,
|
allowed_tools,
|
||||||
permission_mode,
|
permission_mode,
|
||||||
base_commit,
|
base_commit,
|
||||||
reasoning_effort,
|
reasoning_effort,
|
||||||
|
allow_broad_cwd,
|
||||||
)?,
|
)?,
|
||||||
CliAction::HelpTopic(topic) => print_help_topic(topic),
|
CliAction::HelpTopic(topic) => print_help_topic(topic),
|
||||||
CliAction::Help { output_format } => print_help(output_format)?,
|
CliAction::Help { output_format } => print_help(output_format)?,
|
||||||
@@ -327,6 +330,7 @@ enum CliAction {
|
|||||||
compact: bool,
|
compact: bool,
|
||||||
base_commit: Option<String>,
|
base_commit: Option<String>,
|
||||||
reasoning_effort: Option<String>,
|
reasoning_effort: Option<String>,
|
||||||
|
allow_broad_cwd: bool,
|
||||||
},
|
},
|
||||||
Login {
|
Login {
|
||||||
output_format: CliOutputFormat,
|
output_format: CliOutputFormat,
|
||||||
@@ -354,6 +358,7 @@ enum CliAction {
|
|||||||
permission_mode: PermissionMode,
|
permission_mode: PermissionMode,
|
||||||
base_commit: Option<String>,
|
base_commit: Option<String>,
|
||||||
reasoning_effort: Option<String>,
|
reasoning_effort: Option<String>,
|
||||||
|
allow_broad_cwd: bool,
|
||||||
},
|
},
|
||||||
HelpTopic(LocalHelpTopic),
|
HelpTopic(LocalHelpTopic),
|
||||||
// prompt-mode formatting is only supported for non-interactive runs
|
// prompt-mode formatting is only supported for non-interactive runs
|
||||||
@@ -398,6 +403,7 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
|||||||
let mut compact = false;
|
let mut compact = false;
|
||||||
let mut base_commit: Option<String> = None;
|
let mut base_commit: Option<String> = None;
|
||||||
let mut reasoning_effort: Option<String> = None;
|
let mut reasoning_effort: Option<String> = None;
|
||||||
|
let mut allow_broad_cwd = false;
|
||||||
let mut rest: Vec<String> = Vec::new();
|
let mut rest: Vec<String> = Vec::new();
|
||||||
let mut index = 0;
|
let mut index = 0;
|
||||||
|
|
||||||
@@ -510,6 +516,10 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
|||||||
reasoning_effort = Some(value.to_string());
|
reasoning_effort = Some(value.to_string());
|
||||||
index += 1;
|
index += 1;
|
||||||
}
|
}
|
||||||
|
"--allow-broad-cwd" => {
|
||||||
|
allow_broad_cwd = true;
|
||||||
|
index += 1;
|
||||||
|
}
|
||||||
"-p" => {
|
"-p" => {
|
||||||
// Claw Code compat: -p "prompt" = one-shot prompt
|
// Claw Code compat: -p "prompt" = one-shot prompt
|
||||||
let prompt = args[index + 1..].join(" ");
|
let prompt = args[index + 1..].join(" ");
|
||||||
@@ -526,6 +536,7 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
|||||||
compact,
|
compact,
|
||||||
base_commit: base_commit.clone(),
|
base_commit: base_commit.clone(),
|
||||||
reasoning_effort: reasoning_effort.clone(),
|
reasoning_effort: reasoning_effort.clone(),
|
||||||
|
allow_broad_cwd,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
"--print" => {
|
"--print" => {
|
||||||
@@ -597,6 +608,7 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
|||||||
compact: false,
|
compact: false,
|
||||||
base_commit,
|
base_commit,
|
||||||
reasoning_effort,
|
reasoning_effort,
|
||||||
|
allow_broad_cwd,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -606,6 +618,7 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
|||||||
permission_mode,
|
permission_mode,
|
||||||
base_commit,
|
base_commit,
|
||||||
reasoning_effort: reasoning_effort.clone(),
|
reasoning_effort: reasoning_effort.clone(),
|
||||||
|
allow_broad_cwd,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
if rest.first().map(String::as_str) == Some("--resume") {
|
if rest.first().map(String::as_str) == Some("--resume") {
|
||||||
@@ -645,6 +658,7 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
|||||||
compact,
|
compact,
|
||||||
base_commit,
|
base_commit,
|
||||||
reasoning_effort: reasoning_effort.clone(),
|
reasoning_effort: reasoning_effort.clone(),
|
||||||
|
allow_broad_cwd,
|
||||||
}),
|
}),
|
||||||
SkillSlashDispatch::Local => Ok(CliAction::Skills {
|
SkillSlashDispatch::Local => Ok(CliAction::Skills {
|
||||||
args,
|
args,
|
||||||
@@ -671,6 +685,7 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
|||||||
compact,
|
compact,
|
||||||
base_commit: base_commit.clone(),
|
base_commit: base_commit.clone(),
|
||||||
reasoning_effort: reasoning_effort.clone(),
|
reasoning_effort: reasoning_effort.clone(),
|
||||||
|
allow_broad_cwd,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
other if other.starts_with('/') => parse_direct_slash_cli_action(
|
other if other.starts_with('/') => parse_direct_slash_cli_action(
|
||||||
@@ -682,6 +697,7 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
|||||||
compact,
|
compact,
|
||||||
base_commit,
|
base_commit,
|
||||||
reasoning_effort,
|
reasoning_effort,
|
||||||
|
allow_broad_cwd,
|
||||||
),
|
),
|
||||||
_other => Ok(CliAction::Prompt {
|
_other => Ok(CliAction::Prompt {
|
||||||
prompt: rest.join(" "),
|
prompt: rest.join(" "),
|
||||||
@@ -692,6 +708,7 @@ fn parse_args(args: &[String]) -> Result<CliAction, String> {
|
|||||||
compact,
|
compact,
|
||||||
base_commit,
|
base_commit,
|
||||||
reasoning_effort: reasoning_effort.clone(),
|
reasoning_effort: reasoning_effort.clone(),
|
||||||
|
allow_broad_cwd,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -786,6 +803,7 @@ fn parse_direct_slash_cli_action(
|
|||||||
compact: bool,
|
compact: bool,
|
||||||
base_commit: Option<String>,
|
base_commit: Option<String>,
|
||||||
reasoning_effort: Option<String>,
|
reasoning_effort: Option<String>,
|
||||||
|
allow_broad_cwd: bool,
|
||||||
) -> Result<CliAction, String> {
|
) -> Result<CliAction, String> {
|
||||||
let raw = rest.join(" ");
|
let raw = rest.join(" ");
|
||||||
match SlashCommand::parse(&raw) {
|
match SlashCommand::parse(&raw) {
|
||||||
@@ -814,6 +832,7 @@ fn parse_direct_slash_cli_action(
|
|||||||
compact,
|
compact,
|
||||||
base_commit,
|
base_commit,
|
||||||
reasoning_effort: reasoning_effort.clone(),
|
reasoning_effort: reasoning_effort.clone(),
|
||||||
|
allow_broad_cwd,
|
||||||
}),
|
}),
|
||||||
SkillSlashDispatch::Local => Ok(CliAction::Skills {
|
SkillSlashDispatch::Local => Ok(CliAction::Skills {
|
||||||
args,
|
args,
|
||||||
@@ -2899,25 +2918,82 @@ fn run_resume_command(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Stale-base preflight: verify the worktree HEAD matches the expected base
|
/// Detect if the current working directory is "broad" (home directory or
|
||||||
/// commit (from `--base-commit` flag or `.claw-base` file). Emits a warning to
|
/// filesystem root). Returns the cwd path if broad, None otherwise.
|
||||||
/// stderr when the HEAD has diverged.
|
fn detect_broad_cwd() -> Option<PathBuf> {
|
||||||
/// Warn when the working directory is very broad (home directory or filesystem
|
let Ok(cwd) = env::current_dir() else {
|
||||||
/// root). claw scopes its file-system access to the working directory, so
|
return None;
|
||||||
/// starting from a home folder can expose/scan far more than intended.
|
};
|
||||||
fn warn_if_broad_cwd() {
|
|
||||||
let Ok(cwd) = env::current_dir() else { return };
|
|
||||||
let is_home = env::var_os("HOME")
|
let is_home = env::var_os("HOME")
|
||||||
.map(|h| PathBuf::from(h) == cwd)
|
.map(|h| PathBuf::from(h) == cwd)
|
||||||
.unwrap_or(false);
|
.unwrap_or(false);
|
||||||
let is_root = cwd.parent().is_none();
|
let is_root = cwd.parent().is_none();
|
||||||
if is_home || is_root {
|
if is_home || is_root {
|
||||||
|
Some(cwd)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Enforce the broad-CWD policy: when running from home or root, either
|
||||||
|
/// require the --allow-broad-cwd flag, or prompt for confirmation (interactive),
|
||||||
|
/// or exit with an error (non-interactive).
|
||||||
|
fn enforce_broad_cwd_policy(
|
||||||
|
allow_broad_cwd: bool,
|
||||||
|
output_format: CliOutputFormat,
|
||||||
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
|
if allow_broad_cwd {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let Some(cwd) = detect_broad_cwd() else {
|
||||||
|
return Ok(());
|
||||||
|
};
|
||||||
|
|
||||||
|
let is_interactive = io::stdin().is_terminal();
|
||||||
|
|
||||||
|
if is_interactive {
|
||||||
|
// Interactive mode: print warning and ask for confirmation
|
||||||
eprintln!(
|
eprintln!(
|
||||||
"Warning: claw is running from a very broad directory ({}).\n\
|
"Warning: claw is running from a very broad directory ({}).\n\
|
||||||
The agent can read and search everything under this path.\n\
|
The agent can read and search everything under this path.\n\
|
||||||
Consider running from inside your project: cd /path/to/project && claw",
|
Consider running from inside your project: cd /path/to/project && claw",
|
||||||
cwd.display()
|
cwd.display()
|
||||||
);
|
);
|
||||||
|
eprint!("Continue anyway? [y/N]: ");
|
||||||
|
io::stderr().flush()?;
|
||||||
|
|
||||||
|
let mut input = String::new();
|
||||||
|
io::stdin().read_line(&mut input)?;
|
||||||
|
let trimmed = input.trim().to_lowercase();
|
||||||
|
if trimmed != "y" && trimmed != "yes" {
|
||||||
|
eprintln!("Aborted.");
|
||||||
|
std::process::exit(0);
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
} else {
|
||||||
|
// Non-interactive mode: exit with error (JSON or text)
|
||||||
|
let message = format!(
|
||||||
|
"claw is running from a very broad directory ({}). \
|
||||||
|
The agent can read and search everything under this path. \
|
||||||
|
Use --allow-broad-cwd to proceed anyway, \
|
||||||
|
or run from inside your project: cd /path/to/project && claw",
|
||||||
|
cwd.display()
|
||||||
|
);
|
||||||
|
match output_format {
|
||||||
|
CliOutputFormat::Json => {
|
||||||
|
eprintln!(
|
||||||
|
"{}",
|
||||||
|
serde_json::json!({
|
||||||
|
"type": "error",
|
||||||
|
"error": message,
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
CliOutputFormat::Text => {
|
||||||
|
eprintln!("error: {message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
std::process::exit(1);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2939,8 +3015,9 @@ fn run_repl(
|
|||||||
permission_mode: PermissionMode,
|
permission_mode: PermissionMode,
|
||||||
base_commit: Option<String>,
|
base_commit: Option<String>,
|
||||||
reasoning_effort: Option<String>,
|
reasoning_effort: Option<String>,
|
||||||
|
allow_broad_cwd: bool,
|
||||||
) -> Result<(), Box<dyn std::error::Error>> {
|
) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
warn_if_broad_cwd();
|
enforce_broad_cwd_policy(allow_broad_cwd, CliOutputFormat::Text)?;
|
||||||
run_stale_base_preflight(base_commit.as_deref());
|
run_stale_base_preflight(base_commit.as_deref());
|
||||||
let resolved_model = resolve_repl_model(model);
|
let resolved_model = resolve_repl_model(model);
|
||||||
let mut cli = LiveCli::new(resolved_model, true, allowed_tools, permission_mode)?;
|
let mut cli = LiveCli::new(resolved_model, true, allowed_tools, permission_mode)?;
|
||||||
@@ -8461,6 +8538,7 @@ mod tests {
|
|||||||
permission_mode: PermissionMode::DangerFullAccess,
|
permission_mode: PermissionMode::DangerFullAccess,
|
||||||
base_commit: None,
|
base_commit: None,
|
||||||
reasoning_effort: None,
|
reasoning_effort: None,
|
||||||
|
allow_broad_cwd: false,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -8625,6 +8703,7 @@ mod tests {
|
|||||||
compact: false,
|
compact: false,
|
||||||
base_commit: None,
|
base_commit: None,
|
||||||
reasoning_effort: None,
|
reasoning_effort: None,
|
||||||
|
allow_broad_cwd: false,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -8715,6 +8794,7 @@ mod tests {
|
|||||||
compact: false,
|
compact: false,
|
||||||
base_commit: None,
|
base_commit: None,
|
||||||
reasoning_effort: None,
|
reasoning_effort: None,
|
||||||
|
allow_broad_cwd: false,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -8745,6 +8825,7 @@ mod tests {
|
|||||||
compact: true,
|
compact: true,
|
||||||
base_commit: None,
|
base_commit: None,
|
||||||
reasoning_effort: None,
|
reasoning_effort: None,
|
||||||
|
allow_broad_cwd: false,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -8787,6 +8868,7 @@ mod tests {
|
|||||||
compact: false,
|
compact: false,
|
||||||
base_commit: None,
|
base_commit: None,
|
||||||
reasoning_effort: None,
|
reasoning_effort: None,
|
||||||
|
allow_broad_cwd: false,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -8865,6 +8947,7 @@ mod tests {
|
|||||||
permission_mode: PermissionMode::ReadOnly,
|
permission_mode: PermissionMode::ReadOnly,
|
||||||
base_commit: None,
|
base_commit: None,
|
||||||
reasoning_effort: None,
|
reasoning_effort: None,
|
||||||
|
allow_broad_cwd: false,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -8885,6 +8968,7 @@ mod tests {
|
|||||||
permission_mode: PermissionMode::DangerFullAccess,
|
permission_mode: PermissionMode::DangerFullAccess,
|
||||||
base_commit: None,
|
base_commit: None,
|
||||||
reasoning_effort: None,
|
reasoning_effort: None,
|
||||||
|
allow_broad_cwd: false,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -8914,6 +8998,7 @@ mod tests {
|
|||||||
compact: false,
|
compact: false,
|
||||||
base_commit: None,
|
base_commit: None,
|
||||||
reasoning_effort: None,
|
reasoning_effort: None,
|
||||||
|
allow_broad_cwd: false,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -8940,6 +9025,7 @@ mod tests {
|
|||||||
permission_mode: PermissionMode::DangerFullAccess,
|
permission_mode: PermissionMode::DangerFullAccess,
|
||||||
base_commit: None,
|
base_commit: None,
|
||||||
reasoning_effort: None,
|
reasoning_effort: None,
|
||||||
|
allow_broad_cwd: false,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -9050,6 +9136,7 @@ mod tests {
|
|||||||
compact: false,
|
compact: false,
|
||||||
base_commit: None,
|
base_commit: None,
|
||||||
reasoning_effort: None,
|
reasoning_effort: None,
|
||||||
|
allow_broad_cwd: false,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -9434,6 +9521,7 @@ mod tests {
|
|||||||
compact: false,
|
compact: false,
|
||||||
base_commit: None,
|
base_commit: None,
|
||||||
reasoning_effort: None,
|
reasoning_effort: None,
|
||||||
|
allow_broad_cwd: false,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -9501,6 +9589,7 @@ mod tests {
|
|||||||
compact: false,
|
compact: false,
|
||||||
base_commit: None,
|
base_commit: None,
|
||||||
reasoning_effort: None,
|
reasoning_effort: None,
|
||||||
|
allow_broad_cwd: false,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -9527,6 +9616,7 @@ mod tests {
|
|||||||
compact: false,
|
compact: false,
|
||||||
base_commit: None,
|
base_commit: None,
|
||||||
reasoning_effort: None,
|
reasoning_effort: None,
|
||||||
|
allow_broad_cwd: false,
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
let error = parse_args(&["/status".to_string()])
|
let error = parse_args(&["/status".to_string()])
|
||||||
|
|||||||
Reference in New Issue
Block a user