Skip to content

fix: clean up orphan LaunchAgent plist on bootstrap failure#15619

Closed
superlowburn wants to merge 2 commits intoopenclaw:mainfrom
superlowburn:fix/14315-orphan-daemon-cleanup
Closed

fix: clean up orphan LaunchAgent plist on bootstrap failure#15619
superlowburn wants to merge 2 commits intoopenclaw:mainfrom
superlowburn:fix/14315-orphan-daemon-cleanup

Conversation

@superlowburn
Copy link
Copy Markdown
Contributor

@superlowburn superlowburn commented Feb 13, 2026

Summary

Fixes #14315

  • When launchctl bootstrap fails during onboarding (e.g., user denies macOS security prompt), the plist file was left on disk with no cleanup path
  • Now attempts launchctl bootout and removes the plist file before throwing the error
  • Both cleanup operations silently catch errors (file may not exist, service may not be loaded)

Changes

  • src/daemon/launchd.ts: 3 lines added — bootout + unlink on bootstrap failure
  • src/daemon/launchd.test.ts: New test verifying plist cleanup on bootstrap failure

Test plan

  • New test: "cleans up plist file when bootstrap fails" — creates failing launchctl stub, verifies plist is removed
  • All 15 launchd tests pass
  • No existing test assertions changed

🤖 Generated with Claude Code

Greptile Overview

Greptile Summary

This PR updates the macOS LaunchAgent install flow to clean up on launchctl bootstrap failure: it attempts a launchctl bootout and unlinks the generated plist before rethrowing the bootstrap error. It also adds a regression test that stubs launchctl to fail specifically on bootstrap and asserts the plist is removed afterward.

The change is localized to src/daemon/launchd.ts’s installLaunchAgent path and extends the existing launchctl-stubbing test harness in src/daemon/launchd.test.ts.

Confidence Score: 4/5

  • Mostly safe to merge once the Windows test hermeticity issue is addressed.
  • Core runtime change is small and well-scoped, but the new test likely won’t correctly stub launchctl on Windows due to writing an extensionless batch file while execution relies on shell: true PATH resolution. Also noted a misleading dead .catch() in the cleanup path.
  • src/daemon/launchd.test.ts (win32 stub), src/daemon/launchd.ts (bootstrap failure cleanup)

Last reviewed commit: 20dabad

…#14315)

When running interactive onboarding, if the user cancels/denies the macOS
security prompt during `launchctl bootstrap`, the plist file was left on
disk and the daemon could remain partially loaded. This caused ghost
processes to run alongside the intended Docker deployment.

Changes:
- installLaunchAgent now cleans up the plist file if bootstrap fails
- Added bootout attempt to unload any partially loaded service
- Added test to verify plist cleanup on bootstrap failure

Fixes openclaw#14315

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copy link
Copy Markdown
Contributor

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

2 files reviewed, 2 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +243 to +245
// Create a failing launchctl stub (fails on bootstrap command)
const stubPath = path.join(binDir, "launchctl");
const stubScript =
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Windows stub won’t execute

In the new bootstrap-failure test, the win32 branch writes a batch script to binDir/launchctl (no .cmd/.bat). But execLaunchctl runs execFileAsync('launchctl', { shell: true }) on Windows, which typically resolves launchctl.cmd/.bat via PATH and won’t execute an extensionless file. This makes the test non-hermetic (it may call a real launchctl/fail) on Windows; it should create a launchctl.cmd wrapper (like writeLaunchctlStub does) or otherwise ensure the stub is what gets executed.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/daemon/launchd.test.ts
Line: 243:245

Comment:
**Windows stub won’t execute**

In the new bootstrap-failure test, the win32 branch writes a batch script to `binDir/launchctl` (no `.cmd`/`.bat`). But `execLaunchctl` runs `execFileAsync('launchctl', { shell: true })` on Windows, which typically resolves `launchctl.cmd`/`.bat` via `PATH` and won’t execute an extensionless file. This makes the test non-hermetic (it may call a real `launchctl`/fail) on Windows; it should create a `launchctl.cmd` wrapper (like `writeLaunchctlStub` does) or otherwise ensure the stub is what gets executed.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 052b7d5. The stub path now uses launchctl.cmd on Windows (matching the pattern in writeLaunchctlStub which creates both a .js file and a .cmd wrapper).

Comment on lines +439 to +441
// Clean up the plist file since bootstrap failed
await execLaunchctl(["bootout", domain, plistPath]).catch(() => {});
await fs.unlink(plistPath).catch(() => {});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Dead .catch() on cleanup

execLaunchctl never rejects (it catches internally and returns { code, stdout, stderr } even on failure), so await execLaunchctl([...]).catch(() => {}) is dead code and doesn’t change behavior. This is misleading in a failure/cleanup path; either drop the .catch or adjust execLaunchctl/cleanup to use a real throwing call if you intend to ignore exceptions.

Prompt To Fix With AI
This is a comment left during a code review.
Path: src/daemon/launchd.ts
Line: 439:441

Comment:
**Dead `.catch()` on cleanup**

`execLaunchctl` never rejects (it catches internally and returns `{ code, stdout, stderr }` even on failure), so `await execLaunchctl([...]).catch(() => {})` is dead code and doesn’t change behavior. This is misleading in a failure/cleanup path; either drop the `.catch` or adjust `execLaunchctl`/cleanup to use a real throwing call if you intend to ignore exceptions.

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 052b7d5. Removed the dead .catch(() => {})execLaunchctl catches internally and returns { code, stdout, stderr } so it never rejects. The .catch(() => {}) on fs.unlink below is kept since that CAN reject.

@openclaw-barnacle openclaw-barnacle bot added gateway Gateway runtime size: S labels Feb 13, 2026
- Remove dead `.catch(() => {})` on execLaunchctl bootout (it never rejects)
- Add `.cmd` extension for Windows launchctl stub in bootstrap-failure test

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@openclaw-barnacle
Copy link
Copy Markdown

This pull request has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.

@openclaw-barnacle openclaw-barnacle bot added stale Marked as stale due to inactivity and removed stale Marked as stale due to inactivity labels Feb 21, 2026
@openclaw-barnacle
Copy link
Copy Markdown

This pull request has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.

@openclaw-barnacle openclaw-barnacle bot added the stale Marked as stale due to inactivity label Mar 12, 2026
@openclaw-barnacle
Copy link
Copy Markdown

Closing due to inactivity.
If you believe this PR should be revived, post in #pr-thunderdome-dangerzone on Discord to talk to a maintainer.
That channel is the escape hatch for high-quality PRs that get auto-closed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gateway Gateway runtime size: S stale Marked as stale due to inactivity

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Cancelled onboarding leaves orphan launchd daemon running

1 participant