Bug description
PostHog Desktop (Code) recursively deleted my code parent directory: the main git repository including its .git object store, all fifteen git worktree checkouts I had created outside PostHog Desktop, and an unrelated sibling repository. Roughly one million filesystem entries were affected, bypassing the macOS Trash. An unpushed branch containing a local-only commit plus a day of uncommitted work had to be reconstructed from AI-session transcripts.
The app had adopted an existing sibling worktree that I had created outside PostHog Desktop as a promptless idle task. When that task was archived, ArchiveService used the stored external path and then ran:
await manager.deleteWorktree(worktreePath)
await forceRemove(path.dirname(worktreePath))
See archive.ts#L300-L315.
For an app-managed worktree, the expected layout is:
~/.posthog-code/worktrees/<worktree-name>/<repo-name>
For an adopted sibling worktree, mine was structurally:
worktreePath: ~/code/<code-parent>/<repo>.<branch>
dirname: ~/code/<code-parent>
main repo: ~/code/<code-parent>/<repo>
The archive flow therefore passed my entire shared code parent to forceRemove. That function permanently applies recursive fs.rm, including chmod-and-retry handling for resistant trees (utils.ts#L140-L157). This exactly explains the main repo, its .git, every sibling worktree, and the unrelated sibling repository disappearing together.
The same unconditional parent removal exists in automatic suspension (suspension.ts#L314-L321), so worktree-cap or inactivity suspension can produce the same blast radius.
No agent prompt was submitted and no destructive filesystem confirmation was shown. Regardless of whether Archive is invoked intentionally, archiving an adopted checkout must not delete its shared parent directory.
How to reproduce
Only attempt this inside a fully disposable parent directory. The current code recursively removes that parent.
-
Create this disposable layout:
<disposable-parent>/repo
<disposable-parent>/repo.feature # git worktree
<disposable-parent>/unrelated-sentinel
-
Register repo in PostHog Desktop and enable the Worktrees sidebar.
-
Click repo.feature in the Worktrees section. The app creates a promptless idle task and stores the external worktree path (adoption flow).
-
Archive that task. Alternatively, cause it to be auto-suspended.
-
The app deletes <disposable-parent>, including repo and unrelated-sentinel.
This reproduction follows the exact shipped code and is corroborated by the incident artifacts. I have not rerun the destructive flow after identifying it.
Incident evidence
All times CEST on 2026-08-11:
| Time |
Evidence |
| 20:45:40 |
First existing sibling worktree adopted; matching task and run records created. |
| 20:45:44 |
Second existing sibling worktree adopted. |
| 20:46:09.046 |
main.log: (archive) Archiving task … for the first adopted worktree. |
| 20:46:10 |
~/.posthog-code/worktrees created, consistent with moving the adopted checkout into the app-private .trash. |
| 20:46:17.154 |
git worktree list --porcelain fails because the registered main repository’s .git is gone. |
| 20:47:41 |
App relaunched, interrupting the recursive deletion. |
Additional corroboration:
posthog-code.db stores the two adopted worktrees at their exact external sibling paths.
- The ACP session logs contain initialization only—no user prompt.
- The installed
0.60.145 app.asar contains the exact deletion sequence above.
- Fourteen surviving sibling checkout directories had their
.git links removed. The quarantined original main .git retained only partial logs and refs; its object store and worktree registry were gone.
- The
archives table is empty because its row is written only after the awaited parent removal. The app was terminated while that removal was still running.
Raw logs, the SQLite DB, the quarantined tree, and the extracted-bundle comparison are available privately on request.
Expected behavior / fixes
- Never automatically delete an adopted external worktree. Track app-created ownership/provenance; archiving or suspending an adopted checkout should preserve it by default.
- Remove the unconditional
forceRemove(path.dirname(worktreePath)) from both Archive and Suspension.
- If a managed wrapper must be removed, derive it from the configured base and validate the exact
<base>/<name>/<repo> layout with boundary-aware containment checks.
- Add filesystem integration tests for Archive and Suspension using an external worktree plus a sentinel sibling; assert that the parent, main repo, and sentinel survive.
- Require explicit confirmation and use the OS Trash for any user-owned path. Log every deletion with path, provenance, reason, and caller.
Debug info
- [x] PostHog Cloud, region and project ID: EU, 210487
- PostHog Desktop (Code) 0.60.145, commit 3755cd2916b, built 2026-08-11T18:13:48Z
- macOS darwin arm64 24.6.0, Electron 42.4.0, Chromium 148.0.7778.254, Node 24.16.0
Bug description
PostHog Desktop (Code) recursively deleted my code parent directory: the main git repository including its
.gitobject store, all fifteengit worktreecheckouts I had created outside PostHog Desktop, and an unrelated sibling repository. Roughly one million filesystem entries were affected, bypassing the macOS Trash. An unpushed branch containing a local-only commit plus a day of uncommitted work had to be reconstructed from AI-session transcripts.The app had adopted an existing sibling worktree that I had created outside PostHog Desktop as a promptless idle task. When that task was archived,
ArchiveServiceused the stored external path and then ran:See
archive.ts#L300-L315.For an app-managed worktree, the expected layout is:
For an adopted sibling worktree, mine was structurally:
The archive flow therefore passed my entire shared code parent to
forceRemove. That function permanently applies recursivefs.rm, including chmod-and-retry handling for resistant trees (utils.ts#L140-L157). This exactly explains the main repo, its.git, every sibling worktree, and the unrelated sibling repository disappearing together.The same unconditional parent removal exists in automatic suspension (
suspension.ts#L314-L321), so worktree-cap or inactivity suspension can produce the same blast radius.No agent prompt was submitted and no destructive filesystem confirmation was shown. Regardless of whether Archive is invoked intentionally, archiving an adopted checkout must not delete its shared parent directory.
How to reproduce
Only attempt this inside a fully disposable parent directory. The current code recursively removes that parent.
Create this disposable layout:
Register
repoin PostHog Desktop and enable the Worktrees sidebar.Click
repo.featurein the Worktrees section. The app creates a promptless idle task and stores the external worktree path (adoption flow).Archive that task. Alternatively, cause it to be auto-suspended.
The app deletes
<disposable-parent>, includingrepoandunrelated-sentinel.This reproduction follows the exact shipped code and is corroborated by the incident artifacts. I have not rerun the destructive flow after identifying it.
Incident evidence
All times CEST on 2026-08-11:
main.log:(archive) Archiving task …for the first adopted worktree.~/.posthog-code/worktreescreated, consistent with moving the adopted checkout into the app-private.trash.git worktree list --porcelainfails because the registered main repository’s.gitis gone.Additional corroboration:
posthog-code.dbstores the two adopted worktrees at their exact external sibling paths.0.60.145app.asarcontains the exact deletion sequence above..gitlinks removed. The quarantined original main.gitretained only partiallogsandrefs; its object store and worktree registry were gone.archivestable is empty because its row is written only after the awaited parent removal. The app was terminated while that removal was still running.Raw logs, the SQLite DB, the quarantined tree, and the extracted-bundle comparison are available privately on request.
Expected behavior / fixes
forceRemove(path.dirname(worktreePath))from both Archive and Suspension.<base>/<name>/<repo>layout with boundary-aware containment checks.Debug info