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:
2026-04-06 16:01:48 -04:00
parent 75911faeeb
commit 37a1c69e26
2 changed files with 191 additions and 5 deletions
+18 -5
View File
@@ -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()