diff --git a/rust/crates/api/src/providers/mod.rs b/rust/crates/api/src/providers/mod.rs index a928adc7..ca1086d2 100644 --- a/rust/crates/api/src/providers/mod.rs +++ b/rust/crates/api/src/providers/mod.rs @@ -234,7 +234,7 @@ pub fn resolve_model_alias(model: &str) -> String { #[must_use] pub fn metadata_for_model(model: &str) -> Option { let canonical = resolve_model_alias(model); - if canonical.starts_with("claude") { + if canonical.starts_with("claude") || canonical.starts_with("anthropic/") { return Some(ProviderMetadata { provider: ProviderKind::Anthropic, auth_env: "ANTHROPIC_API_KEY", diff --git a/rust/crates/rusty-claude-cli/src/main.rs b/rust/crates/rusty-claude-cli/src/main.rs index fc9c071a..387c6d7f 100644 --- a/rust/crates/rusty-claude-cli/src/main.rs +++ b/rust/crates/rusty-claude-cli/src/main.rs @@ -292,6 +292,8 @@ fn classify_error_kind(message: &str) -> &'static str { "malformed_mcp_config" } else if message.starts_with("empty prompt") { "empty_prompt" + } else if message.starts_with("interactive_only:") || message.contains("stdin is not a TTY") { + "interactive_only" } else { "unknown" } @@ -940,6 +942,12 @@ fn parse_args(args: &[String]) -> Result { allow_broad_cwd, }); } + // Non-TTY stdin with no piped content: refuse to start the interactive + // REPL (it would block forever waiting for input that will never arrive). + // (#696: emit a typed error instead of hanging indefinitely) + // Skip this guard in test builds (parse_args tests run in non-TTY context). + #[cfg(not(test))] + return Err("interactive_only: claw requires an interactive terminal (stdin is not a TTY and no prompt was provided — pipe a prompt or run in a TTY)".into()); } return Ok(CliAction::Repl { model,