diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index 9385db8..738d7c5 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -447,11 +447,14 @@ fn parse_args(args: &[String]) -> Result { let value = args .get(index + 1) .ok_or_else(|| "missing value for --model".to_string())?; + validate_model_syntax(value)?; model = resolve_model_alias_with_config(value); index += 2; } flag if flag.starts_with("--model=") => { - model = resolve_model_alias_with_config(&flag[8..]); + let value = &flag[8..]; + validate_model_syntax(value)?; + model = resolve_model_alias_with_config(value); index += 1; } "--output-format" => { @@ -1035,6 +1038,37 @@ fn resolve_model_alias_with_config(model: &str) -> String { resolve_model_alias(trimmed).to_string() } +/// Validate model syntax at parse time. +/// Accepts: known aliases (opus, sonnet, haiku) or provider/model pattern. +/// Rejects: empty, whitespace-only, strings with spaces, or invalid chars. +fn validate_model_syntax(model: &str) -> Result<(), String> { + let trimmed = model.trim(); + if trimmed.is_empty() { + return Err("model string cannot be empty".to_string()); + } + // Known aliases are always valid + match trimmed { + "opus" | "sonnet" | "haiku" => return Ok(()), + _ => {} + } + // Check for spaces (malformed) + if trimmed.contains(' ') { + return Err(format!( + "invalid model syntax: '{}' contains spaces. Use provider/model format or known alias", + trimmed + )); + } + // Check provider/model format: provider_id/model_id + let parts: Vec<&str> = trimmed.split('/').collect(); + if parts.len() != 2 || parts[0].is_empty() || parts[1].is_empty() { + return Err(format!( + "invalid model syntax: '{}'. Expected provider/model (e.g., anthropic/claude-opus-4-6) or known alias (opus, sonnet, haiku)", + trimmed + )); + } + Ok(()) +} + fn config_alias_for_current_dir(alias: &str) -> Option { if alias.is_empty() { return None;