You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Teardown scripts are skipped when a workspace is deleted via MCP/CLI/SDK (force: true is hardcoded) #6174
.superset/config.json teardown commands (and .superset/teardown.sh) run correctly when a workspace is deleted from the Superset app UI, but are silently skipped when the workspace is deleted through the MCP tool workspaces_delete (and, from reading the code, the CLI/SDK surface too).
Actual: the worktree is removed, the response is {"success":true,...,"warnings":[]}, and the teardown script never executes — external resources it should clean up (in our case a Herd site link and a cloned MySQL database) are left behind. There is no trace in host-service.log and no warning anywhere.
Root cause
In @superset/host-service (src/trpc/router/workspace-cleanup/workspace-cleanup.ts), runDestroy skips step 1 (teardown) when force is set. The external delete surface hardcodes it:
// Legacy external surface used by CLI/SDK/MCP. Preserve its// non-interactive contract while reusing the v2 cleanup path.returndestroyWorkspace(ctx,{workspaceId: input.id,deleteBranch: false,force: true,});
So every non-interactive delete is a force delete, and force semantics ("skips teardown (step 1)") turn the documented teardown contract into UI-only behavior.
Why this matters
The MCP surface is exactly the one agents use for automated workspace lifecycles — the flow that most needs deterministic cleanup. Provisioned per-workspace resources (databases, vhosts, tunnels) leak on every programmatic delete, and the empty warnings array makes it look like everything was cleaned up.
Suggested fix
Run teardown on the non-interactive path too, e.g.:
default force: false for the external surface and expose an optional force parameter on workspaces_delete, mapping a teardown failure to a typed error the caller can retry with force: true; or
keep force: true for the worktree/branch semantics but still attempt teardown (best-effort), surfacing a failure in warnings.
Either would restore the documented behavior for programmatic deletes.
Version: Superset.app 1.16.1 on macOS (Darwin 25.4.0). Workaround we use meanwhile: open a terminal in the workspace, run the teardown script, wait, then call workspaces_delete.
The report reproduces from the code. Here's my triage:
Confirmed by reading the code — this is a real behavioral gap, and the root cause identified in the report is exactly right.
Summary
.superset teardown commands only run on the interactive delete path. Desktop and mobile call workspaceCleanup.destroy with force defaulting to false, so teardown executes; every programmatic surface (MCP workspaces_delete, superset workspaces delete, SDK workspaces.delete) funnels through the legacy workspace.delete procedure, which hardcodes force: true. Since force is the same flag that means "skip teardown", the documented "delete workspace → teardown commands run" contract is unreachable from any non-UI caller, and because teardown is skipped rather than attempted-and-failed, warnings stays empty and nothing is logged — the caller gets a clean {"success":true,...,"warnings":[]} while externally provisioned resources leak.
packages/host-service/src/trpc/router/workspace-cleanup/workspace-cleanup.ts:237 — teardown is gated on if (!input.force && local && project), and the doc comment at :136-138 states force "skips teardown (step 1)".
Callers of the forced path:
packages/mcp/src/tools/workspaces/delete.ts:33 — "workspace.delete", no force in inputSchema
Callers of the correct path (teardown runs): apps/desktop/src/renderer/hooks/host-service/useDestroyWorkspace/useDestroyWorkspace.ts:71-75 (force: input.force ?? false) and apps/mobile/.../useWorkspaceHeaderActions.ts:83-91.
Root cause
One flag carries two unrelated meanings in runDestroy. force is used both for "the user consented to destructive git semantics" (skip the dirty-worktree preflight at :201, --force --force on worktree removal at :278-281) and for "skip the user's teardown script" (:237). The legacy external surface wants only the first — its intent is a non-interactive contract that can't stop to prompt about uncommitted changes — but it inherits the second for free. Nothing in runTeardown is at fault: packages/host-service/src/runtime/teardown/teardown.ts:54 is simply never called on this path, which is also why host-service.log shows no trace.
Note the legacy in-process desktop path at apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts:222-259 models this correctly: it always runs teardown and uses force only to decide whether a teardown failure blocks the delete.
Reproduction
Integration-test sketch, closely following the existing packages/host-service/test/integration/teardown.integration.test.ts and workspace-create-delete.integration.test.ts fixtures:
// Create a feature worktree scenario whose repo has// .superset/config.json → { "teardown": ["touch $PWD/teardown-ran"] }constscenario=awaitcreateFeatureWorktreeScenario({hostOptions: {apiOverrides: cloudFlows.workspaceDeleteOk()},});awaitscenario.host.trpc.workspace.delete.mutate({id: scenario.featureWorkspaceId,});// Fails today: teardown never runs on the external surface.expect(existsSync(join(someDurablePath,"teardown-ran"))).toBe(true);
(Assert against a marker outside the worktree, since step 2b deletes the worktree directory.) Manually: any project with a teardown entry, delete via MCP workspaces_delete vs. the UI delete dialog and compare.
Fix direction
The cleanest fix is to split the two meanings rather than flip force wholesale — the external surface genuinely needs to bypass the dirty-worktree preflight, so force: false there would start rejecting deletes that currently succeed. Suggested shape:
Add a third field to DestroyWorkspaceInput (e.g. skipTeardown: boolean) and gate :237 on that instead of input.force. workspaceCleanup.destroy maps skipTeardown: input.force to preserve the renderer's current force-retry contract; workspace.delete passes force: true, skipTeardown: false.
On the non-interactive path, a teardown failure shouldn't hard-fail the delete (there's no caller to prompt for force-retry) — push it into warnings (Teardown failed (exit N): …) so the MCP/CLI/SDK response stops claiming a clean cleanup. That also fixes the "empty warnings looks successful" half of the report independently.
Optionally expose force/skipTeardown on workspaces_delete's inputSchema so an agent can opt out deliberately.
Two existing tests encode the current behavior and will need updating alongside: packages/host-service/test/integration/bug-hunt-3.integration.test.ts:130 ("workspace.delete forces removal through the v2 cleanup saga", asserts warnings is empty on a dirty worktree) and workspace-create-delete.integration.test.ts:394. Both should keep passing under the split above, since neither fixture configures a teardown — worth confirming resolveTeardownCommand returns null for them so teardown stays a no-op { status: "skipped" }.
The docs at apps/docs/content/docs/setup-teardown-scripts.mdx:169-176 describe force-delete purely as a UI recovery affordance for a failed teardown, which supports reading the current programmatic behavior as unintended rather than a deliberate contract.
Activity
github-actions commentedon Aug 5, 2026
The report reproduces from the code. Here's my triage:
Confirmed by reading the code — this is a real behavioral gap, and the root cause identified in the report is exactly right.
Summary
.supersetteardown commands only run on the interactive delete path. Desktop and mobile callworkspaceCleanup.destroywithforcedefaulting tofalse, so teardown executes; every programmatic surface (MCPworkspaces_delete,superset workspaces delete, SDKworkspaces.delete) funnels through the legacyworkspace.deleteprocedure, which hardcodesforce: true. Sinceforceis the same flag that means "skip teardown", the documented "delete workspace → teardown commands run" contract is unreachable from any non-UI caller, and because teardown is skipped rather than attempted-and-failed,warningsstays empty and nothing is logged — the caller gets a clean{"success":true,...,"warnings":[]}while externally provisioned resources leak.Affected code
packages/host-service/src/trpc/router/workspace/workspace.ts:159-169—workspace.deletehardcodesforce: true:packages/host-service/src/trpc/router/workspace-cleanup/workspace-cleanup.ts:237— teardown is gated onif (!input.force && local && project), and the doc comment at:136-138states force "skips teardown (step 1)".packages/mcp/src/tools/workspaces/delete.ts:33—"workspace.delete", noforceininputSchemapackages/cli/src/commands/workspaces/delete/command.ts:34packages/sdk/src/resources/workspaces.ts:122apps/desktop/src/renderer/hooks/host-service/useDestroyWorkspace/useDestroyWorkspace.ts:71-75(force: input.force ?? false) andapps/mobile/.../useWorkspaceHeaderActions.ts:83-91.Root cause
One flag carries two unrelated meanings in
runDestroy.forceis used both for "the user consented to destructive git semantics" (skip the dirty-worktree preflight at:201,--force --forceon worktree removal at:278-281) and for "skip the user's teardown script" (:237). The legacy external surface wants only the first — its intent is a non-interactive contract that can't stop to prompt about uncommitted changes — but it inherits the second for free. Nothing inrunTeardownis at fault:packages/host-service/src/runtime/teardown/teardown.ts:54is simply never called on this path, which is also whyhost-service.logshows no trace.Note the legacy in-process desktop path at
apps/desktop/src/lib/trpc/routers/workspaces/procedures/delete.ts:222-259models this correctly: it always runs teardown and usesforceonly to decide whether a teardown failure blocks the delete.Reproduction
Integration-test sketch, closely following the existing
packages/host-service/test/integration/teardown.integration.test.tsandworkspace-create-delete.integration.test.tsfixtures:(Assert against a marker outside the worktree, since step 2b deletes the worktree directory.) Manually: any project with a
teardownentry, delete via MCPworkspaces_deletevs. the UI delete dialog and compare.Fix direction
The cleanest fix is to split the two meanings rather than flip
forcewholesale — the external surface genuinely needs to bypass the dirty-worktree preflight, soforce: falsethere would start rejecting deletes that currently succeed. Suggested shape:DestroyWorkspaceInput(e.g.skipTeardown: boolean) and gate:237on that instead ofinput.force.workspaceCleanup.destroymapsskipTeardown: input.forceto preserve the renderer's current force-retry contract;workspace.deletepassesforce: true, skipTeardown: false.warnings(Teardown failed (exit N): …) so the MCP/CLI/SDK response stops claiming a clean cleanup. That also fixes the "emptywarningslooks successful" half of the report independently.force/skipTeardownonworkspaces_delete'sinputSchemaso an agent can opt out deliberately.Two existing tests encode the current behavior and will need updating alongside:
packages/host-service/test/integration/bug-hunt-3.integration.test.ts:130("workspace.delete forces removal through the v2 cleanup saga", assertswarningsis empty on a dirty worktree) andworkspace-create-delete.integration.test.ts:394. Both should keep passing under the split above, since neither fixture configures a teardown — worth confirmingresolveTeardownCommandreturnsnullfor them so teardown stays a no-op{ status: "skipped" }.The docs at
apps/docs/content/docs/setup-teardown-scripts.mdx:169-176describe force-delete purely as a UI recovery affordance for a failed teardown, which supports reading the current programmatic behavior as unintended rather than a deliberate contract.Kitenite commentedon Aug 5, 2026
Hey @eckelarsson thanks for reporting, fixing this immediately
fix(host-service): run teardown scripts on CLI/SDK/MCP workspace dele…
fix(mobile): Codex stops scrolling after a terminal tab switch (#8737)