feat: Workspace filesystem cleanup #391
8 issues
Medium
Test failure fixture adds new failures and changes counts without retention path log update - `src/snapshot-tests/__fixtures__/mcp/simulator/test--failure.txt:9-37`
The fixture now reports 4 failures (was 2) and 57 discovered tests (was 52), and adds two new failure entries plus a workspace-scoped DerivedData path. Per the skill guardrails, fixture updates must map to intentional behavior changes and MCP/CLI/JSON fixtures must stay aligned. The change to test counts/failures appears unrelated to the workspace filesystem cleanup PR's stated scope (workspace path changes), suggesting either a fixture-only update to make tests pass or an unintentional change in the underlying test suite. Verify the corresponding CLI and JSON fixtures for simulator/test--failure reflect the same new failures and counts; otherwise the envelopes are misaligned.
forceStopDaemon may leave socket and registry in place when process hasn't exited within 500ms - `src/cli/daemon-control.ts:53-57`
forceStopDaemon sends SIGTERM, waits only 500ms, then calls cleanupWorkspaceDaemonFiles without allowLiveOwner: true. In daemon-registry.ts, canRemoveRegistryEntry requires !isPidAlive(entry.pid) when allowLiveOwner is not set, so if the daemon hasn't died yet the registry entry and socket file are not removed. Since forceStopDaemon's documented purpose is to remove the stale socket after a version-mismatch force-stop, callers (e.g. ensureDaemonRunning) may immediately try to start a new daemon while the old socket is still present, defeating the cleanup.
Also found at:
src/daemon.ts:334-351
ensureSocketDir TOCTOU lets a pre-existing directory bypass the 0o700 mode - `src/daemon/socket-path.ts:29-36`
ensureSocketDir() guards mkdirSync with existsSync(dir); if the directory already exists (e.g., created by another local user in the shared tmpdir before the daemon starts) the function returns without enforcing mode 0o700 or verifying ownership. Combined with the move of the socket directory under os.tmpdir() in daemonDirForWorkspaceKey(), a local attacker can pre-create the per-workspace directory world-readable/writable and the daemon will happily place its UNIX socket and registry inside it, exposing IPC traffic.
runningScheduledSweeps lock leaks when sweep promise is never created - `src/utils/workspace-filesystem-lifecycle.ts:471-488`
In scheduleWorkspaceFilesystemLifecycleSweep, runningScheduledSweeps.add(scheduleKey) is called synchronously, but the corresponding delete only fires inside the setTimeout's .finally(). If the timer is cancelled (e.g. process shutdown before delay elapses) or if runWorkspaceFilesystemLifecycleSweep throws synchronously before returning a promise, the scheduleKey is never removed and all future scheduling for that workspace/logDir is silently blocked for the lifetime of the process.
Also found at:
src/utils/fs-lock.ts:138-148
Low
New 'Running tests' progress lines added to fixture without normalization - `src/snapshot-tests/__fixtures__/cli/macos/test--success.txt:18-19`
The fixture now includes two 'Running tests (N completed, 0 failures, 0 skipped)' progress lines that did not previously appear. If these progress messages can vary in count or ordering between runs (e.g. flushed differently across platforms), they may be volatile and should be normalized in code rather than baked into the fixture. The Derived Data path change to workspace-scoped layout is consistent with the PR intent. Reviewers should confirm these progress lines are deterministic for this scheme; otherwise normalization belongs in the rendering pipeline, not the fixture.
Successful recovery on second attempt is discarded without retrying createLock - `src/utils/fs-lock.ts:209-228`
In tryAcquireFsLock the retry loop runs only attempts 0 and 1. If attempt=1 hits EEXIST and tryRecoverExpiredLockDir succeeds, the loop ends (attempt becomes 2) without performing the createLock that the recovery enabled. The function then falls through to return null, so a recoverable lock is reported as unacquirable and callers must retry, defeating the purpose of the second attempt.
Also found at:
src/utils/fs-lock.ts:76-95
buildSchedulePreKey cooldown can be bypassed by alternating workspaceKey and logDir - `src/utils/workspace-filesystem-lifecycle.ts:446-454`
buildSchedulePreKey returns either a workspace: or logDir: key based on which option is present. A caller that schedules first with {workspaceKey} and then with {logDir} (pointing to the same workspace's logs) will produce different preKeys, defeating the cooldown. Combined with the separate scheduleKey (workspaceKey:logDir), this allows scheduling far more sweeps than intended for the same underlying directory.
Also found at:
src/utils/workspace-filesystem-lifecycle.ts:492-521
Nested ternary reduces clarity in shouldRecoverLockDir - `src/utils/fs-lock.ts:80-84`
The early-return branch uses a ternary expression wrapping an awaited call to construct the result object, which the skill explicitly discourages in favor of clear if/else chains. Replacing it with explicit if/else improves readability and matches the project's code-simplifier guidance. User-visible impact: maintainers must mentally parse the ternary plus object shorthand to understand control flow.
Also found at:
src/daemon.ts:152-171src/utils/runtime-instance.ts:53-55
14 skills analyzed
| Skill | Findings | Duration | Cost |
|---|---|---|---|
| xcodebuildmcp-docs-release-review | 0 | 41.1s | $0.02 |
| xcodebuildmcp-docs-command-review | 0 | 42.8s | $0.08 |
| xcodebuildmcp-runtime-boundary-review | 0 | 3m 24s | $0.37 |
| xcodebuildmcp-snapshot-fixture-review | 2 | 5m 23s | $2.80 |
| xcodebuildmcp-structured-output-review | 0 | 43.7s | $0.95 |
| xcodebuildmcp-test-boundary-review | 0 | 3m 11s | $3.07 |
| xcodebuildmcp-tool-contract-review | 0 | 3m 28s | $0.06 |
| wrdn-pii | 0 | 10m 54s | $5.51 |
| wrdn-authz | 0 | 6m 7s | $1.67 |
| wrdn-code-execution | 0 | 6m 54s | $3.73 |
| wrdn-data-exfil | 0 | 7m 42s | $1.54 |
| find-bugs | 5 | 12m 40s | $4.37 |
| code-review | 0 | 13m 38s | $2.18 |
| code-simplifier | 1 | 14m 15s | $1.12 |
Duration: 89m 45s · Tokens: 12.9M in / 35.4k out · Cost: $27.52 (+merge: $0.01, +dedup: $0.03)