fix: active workspace delete protection now checks manifest file, not just env var
Root cause: CTASK_WORKSPACE env var only exists inside the child session spawned by ctask resume. A separate terminal window does not inherit it, so the env-var check was bypassed entirely. os.RemoveAll then deleted all accessible files while the root dir was locked by the active cmd.exe process. Fix: Add a second protection check for .ctask/manifest-start.json, which is only present during a live session. Both checks run before any mutation (summary scan, confirmation, or deletion). Either check triggers immediate refusal. Tests: 4 new tests covering manifest-based protection, no-file-mutation on refusal, env-var protection, and normal deletion of inactive workspaces. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
+18
-5
@@ -35,11 +35,17 @@ func runDelete(cmd *cobra.Command, args []string) error {
|
||||
root := config.ResolveRoot()
|
||||
ws := resolveOne(root, args[0], deleteAll)
|
||||
|
||||
// Active workspace protection: refuse if this is the current session workspace
|
||||
// Active workspace protection — two checks, BOTH run before any mutation.
|
||||
//
|
||||
// Check 1: CTASK_WORKSPACE env var matches (catches same-session delete attempts)
|
||||
// Check 2: .ctask/manifest-start.json exists (catches cross-terminal delete attempts
|
||||
// because the manifest is only present during a live session)
|
||||
//
|
||||
// Either check triggers immediate refusal with no side effects.
|
||||
absTarget, _ := filepath.Abs(ws.Path)
|
||||
|
||||
activeWs := os.Getenv("CTASK_WORKSPACE")
|
||||
if activeWs != "" {
|
||||
// Normalize both paths for comparison
|
||||
absTarget, _ := filepath.Abs(ws.Path)
|
||||
absActive, _ := filepath.Abs(activeWs)
|
||||
if strings.EqualFold(absTarget, absActive) {
|
||||
fmt.Fprintln(os.Stderr, "Cannot delete the active workspace. Exit the session first.")
|
||||
@@ -47,9 +53,17 @@ func runDelete(cmd *cobra.Command, args []string) error {
|
||||
}
|
||||
}
|
||||
|
||||
manifestPath := filepath.Join(ws.Path, ".ctask", "manifest-start.json")
|
||||
if _, err := os.Stat(manifestPath); err == nil {
|
||||
fmt.Fprintln(os.Stderr, "Cannot delete a workspace with an active session. Exit the session first.")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// --- Past this point, we are confident no session is active. ---
|
||||
|
||||
relPath := workspace.RelativePath(root, ws.Path)
|
||||
|
||||
// Gather contents summary (best-effort)
|
||||
// Gather contents summary (best-effort, read-only)
|
||||
fileCount, totalSize := summarizeContents(ws.Path)
|
||||
|
||||
fmt.Println()
|
||||
@@ -68,7 +82,6 @@ func runDelete(cmd *cobra.Command, args []string) error {
|
||||
best = r
|
||||
}
|
||||
}
|
||||
absTarget, _ := filepath.Abs(ws.Path)
|
||||
absBest, _ := filepath.Abs(best.Path)
|
||||
if strings.EqualFold(absTarget, absBest) {
|
||||
fmt.Println()
|
||||
|
||||
Reference in New Issue
Block a user