Просмотр исходного кода

Prevent accidental session clears in REPL and resume flows

Require an explicit /clear --confirm flag before wiping live or resumed session state. This keeps the command genuinely useful while adding the minimal safety check needed for a destructive command in a chatty terminal workflow.

Constraint: /clear must remain a real functional command without introducing interactive prompt machinery that would complicate REPL input handling
Rejected: Add y/n interactive confirmation prompt | extra stateful prompting would be slower to ship and more fragile inside the line editor loop
Confidence: high
Scope-risk: narrow
Reversibility: clean
Directive: Keep destructive slash commands opt-in via explicit flags unless the CLI gains a dedicated confirmation subsystem
Tested: cargo fmt --manifest-path ./rust/Cargo.toml --all; cargo clippy --manifest-path ./rust/Cargo.toml --workspace --all-targets -- -D warnings; cargo test --manifest-path ./rust/Cargo.toml --workspace
Not-tested: Manual keyboard-driven UX pass for accidental /clear entry in interactive REPL
Yeachan-Heo 2 месяцев назад
Родитель
Сommit
0ac188caad
2 измененных файлов с 54 добавлено и 10 удалено
  1. 19 6
      rust/crates/commands/src/lib.rs
  2. 35 4
      rust/crates/rusty-claude-cli/src/main.rs

+ 19 - 6
rust/crates/commands/src/lib.rs

@@ -72,7 +72,7 @@ const SLASH_COMMAND_SPECS: &[SlashCommandSpec] = &[
     SlashCommandSpec {
         name: "clear",
         summary: "Start a fresh local session",
-        argument_hint: None,
+        argument_hint: Some("[--confirm]"),
         resume_supported: true,
     },
     SlashCommandSpec {
@@ -114,7 +114,7 @@ pub enum SlashCommand {
     Compact,
     Model { model: Option<String> },
     Permissions { mode: Option<String> },
-    Clear,
+    Clear { confirm: bool },
     Cost,
     Resume { session_path: Option<String> },
     Config,
@@ -143,7 +143,9 @@ impl SlashCommand {
             "permissions" => Self::Permissions {
                 mode: parts.next().map(ToOwned::to_owned),
             },
-            "clear" => Self::Clear,
+            "clear" => Self::Clear {
+                confirm: parts.next() == Some("--confirm"),
+            },
             "cost" => Self::Cost,
             "resume" => Self::Resume {
                 session_path: parts.next().map(ToOwned::to_owned),
@@ -225,7 +227,7 @@ pub fn handle_slash_command(
         SlashCommand::Status
         | SlashCommand::Model { .. }
         | SlashCommand::Permissions { .. }
-        | SlashCommand::Clear
+        | SlashCommand::Clear { .. }
         | SlashCommand::Cost
         | SlashCommand::Resume { .. }
         | SlashCommand::Config
@@ -263,7 +265,14 @@ mod tests {
                 mode: Some("read-only".to_string()),
             })
         );
-        assert_eq!(SlashCommand::parse("/clear"), Some(SlashCommand::Clear));
+        assert_eq!(
+            SlashCommand::parse("/clear"),
+            Some(SlashCommand::Clear { confirm: false })
+        );
+        assert_eq!(
+            SlashCommand::parse("/clear --confirm"),
+            Some(SlashCommand::Clear { confirm: true })
+        );
         assert_eq!(SlashCommand::parse("/cost"), Some(SlashCommand::Cost));
         assert_eq!(
             SlashCommand::parse("/resume session.json"),
@@ -285,7 +294,7 @@ mod tests {
         assert!(help.contains("/compact"));
         assert!(help.contains("/model [model]"));
         assert!(help.contains("/permissions [read-only|workspace-write|danger-full-access]"));
-        assert!(help.contains("/clear"));
+        assert!(help.contains("/clear [--confirm]"));
         assert!(help.contains("/cost"));
         assert!(help.contains("/resume <session-path>"));
         assert!(help.contains("/config"));
@@ -349,6 +358,10 @@ mod tests {
         )
         .is_none());
         assert!(handle_slash_command("/clear", &session, CompactionConfig::default()).is_none());
+        assert!(
+            handle_slash_command("/clear --confirm", &session, CompactionConfig::default())
+                .is_none()
+        );
         assert!(handle_slash_command("/cost", &session, CompactionConfig::default()).is_none());
         assert!(handle_slash_command(
             "/resume session.json",

+ 35 - 4
rust/crates/rusty-claude-cli/src/main.rs

@@ -299,7 +299,15 @@ fn run_resume_command(
                 message: Some(result.message),
             })
         }
-        SlashCommand::Clear => {
+        SlashCommand::Clear { confirm } => {
+            if !confirm {
+                return Ok(ResumeCommandOutcome {
+                    session: session.clone(),
+                    message: Some(
+                        "clear: confirmation required; rerun with /clear --confirm".to_string(),
+                    ),
+                });
+            }
             let cleared = Session::new();
             cleared.save_to_path(session_path)?;
             Ok(ResumeCommandOutcome {
@@ -448,7 +456,7 @@ impl LiveCli {
             SlashCommand::Compact => self.compact()?,
             SlashCommand::Model { model } => self.set_model(model)?,
             SlashCommand::Permissions { mode } => self.set_permissions(mode)?,
-            SlashCommand::Clear => self.clear_session()?,
+            SlashCommand::Clear { confirm } => self.clear_session(confirm)?,
             SlashCommand::Cost => self.print_cost(),
             SlashCommand::Resume { session_path } => self.resume_session(session_path)?,
             SlashCommand::Config => Self::print_config()?,
@@ -526,7 +534,14 @@ impl LiveCli {
         Ok(())
     }
 
-    fn clear_session(&mut self) -> Result<(), Box<dyn std::error::Error>> {
+    fn clear_session(&mut self, confirm: bool) -> Result<(), Box<dyn std::error::Error>> {
+        if !confirm {
+            println!(
+                "clear: confirmation required; run /clear --confirm to start a fresh session."
+            );
+            return Ok(());
+        }
+
         self.runtime = build_runtime_with_permission_mode(
             Session::new(),
             self.model.clone(),
@@ -1274,7 +1289,7 @@ mod tests {
         assert!(help.contains("/status"));
         assert!(help.contains("/model [model]"));
         assert!(help.contains("/permissions [read-only|workspace-write|danger-full-access]"));
-        assert!(help.contains("/clear"));
+        assert!(help.contains("/clear [--confirm]"));
         assert!(help.contains("/cost"));
         assert!(help.contains("/resume <session-path>"));
         assert!(help.contains("/config"));
@@ -1367,6 +1382,18 @@ mod tests {
         assert_eq!(normalize_permission_mode("unknown"), None);
     }
 
+    #[test]
+    fn clear_command_requires_explicit_confirmation_flag() {
+        assert_eq!(
+            SlashCommand::parse("/clear"),
+            Some(SlashCommand::Clear { confirm: false })
+        );
+        assert_eq!(
+            SlashCommand::parse("/clear --confirm"),
+            Some(SlashCommand::Clear { confirm: true })
+        );
+    }
+
     #[test]
     fn parses_resume_and_config_slash_commands() {
         assert_eq!(
@@ -1375,6 +1402,10 @@ mod tests {
                 session_path: Some("saved-session.json".to_string())
             })
         );
+        assert_eq!(
+            SlashCommand::parse("/clear --confirm"),
+            Some(SlashCommand::Clear { confirm: true })
+        );
         assert_eq!(SlashCommand::parse("/config"), Some(SlashCommand::Config));
         assert_eq!(SlashCommand::parse("/memory"), Some(SlashCommand::Memory));
         assert_eq!(SlashCommand::parse("/init"), Some(SlashCommand::Init));