Skip to content

Windows: failed ocx service install leaves proxy stopped and Codex routing un-injected because teardown runs before the elevation check #1454

Closed

Description

@MarcusNeufeldt

Client or integration

Codex CLI

Area

Service lifecycle

Summary

On Windows, ocx service install stops the proxy and removes opencodex's injected Codex config keys before it discovers it needs elevation. When the schtasks call is then denied, the teardown is not undone — so a failed install leaves a previously working setup broken.

Observed sequence:

  1. ocx service install stops the running proxy and restores native Codex routing (removing openai_base_url and model_catalog_json from <CODEX_HOME>/config.toml).
  2. It then runs schtasks /create ..., which fails because the shell is not elevated:
❌ Service install cleanup failed: Windows access denied while running Task Scheduler.
Command: schtasks /create /tn opencodex-proxy /xml <OPENCODEX_HOME>\opencodex-service-task.xml /f
Approve the Windows UAC prompt to install the background service, or run `ocx service install`
from an elevated PowerShell window.
OCX_ERROR_CODE=WINDOWS_SCHTASKS_CREATE_ACCESS_DENIED
  1. The service is not installed and the proxy is left stopped with routing removed. ocx status afterwards reports Restart safety: native Codex routing (no opencodex restart dependency).

Expected: elevation is checked (or the task creation attempted) before any teardown, so that a permission failure is a no-op; or the teardown is rolled back when install fails.

The message text does anticipate the failure ("Approve the Windows UAC prompt… or run from an elevated PowerShell window"), so the elevation requirement is known in advance — it just is not checked early enough.

Reproduction

100% reproducible on my machine.

  1. Have a working opencodex install with the proxy running and Codex routed through it.
  2. From a non-elevated shell, run ocx service install.
  3. Observe the access-denied error above.
  4. Check <CODEX_HOME>/config.toml — openai_base_url and model_catalog_json are gone.
  5. Check the proxy — stopped.

Recovering is harder than it should be because ocx restore back and ocx sync both refuse afterwards with a message naming no reason (filed separately). I restored routing by hand using the keys in <CODEX_HOME>/opencodex.config.toml.

Suggested fixes

  1. Probe for elevation (or attempt the schtasks create) before stopping the proxy and un-injecting the config. A permissions failure should leave the system exactly as it was found.
  2. Failing that, make the cleanup path actually restore what the install path removed — the current message says "Service install cleanup failed", so a cleanup step exists but does not cover the config/proxy teardown.
  3. Consider detecting non-elevated invocation up front and printing the "run from an elevated PowerShell window" guidance instead of starting the install at all.

Version

2.11.0 (the install path is unchanged in 2.12.0 as far as I can tell from the release notes)

Operating system

Windows 11 Pro 10.0.26200

Provider and model

Not provider-specific.

Logs or error output

$ ocx health --json
{"ok":true,"pid":19940,"port":10100}

$ ocx service install
❌ Service install cleanup failed: Windows access denied while running Task Scheduler.
Command: schtasks /create /tn opencodex-proxy /xml <OPENCODEX_HOME>\opencodex-service-task.xml /f
Approve the Windows UAC prompt to install the background service, or run `ocx service install`
from an elevated PowerShell window.
OCX_ERROR_CODE=WINDOWS_SCHTASKS_CREATE_ACCESS_DENIED

$ ocx health --json
{"ok":false,"pid":null,"port":null}

$ ocx service status
❌ service not installed (Task Scheduler).

$ ocx status
❌ Proxy: not running
   Restart safety: native Codex routing (no opencodex restart dependency)
   Service: not installed

# and the injected keys are gone from <CODEX_HOME>/config.toml:
#   openai_base_url      -> removed
#   model_catalog_json   -> removed

Screenshots and supporting files

No response

Redacted configuration

{
  "providers": {
    "openai": { "adapter": "openai-responses", "codexAccountMode": "direct", "authMode": "forward" }
  },
  "codexAutoStart": true,
  "activeCodexAccountId": "__main__"
}

Windows sandbox mode is unelevated in the Codex config; the shell used was a normal (non-admin) terminal.

Checks

  • I searched existing issues and documentation.
  • I removed secrets, tokens, account details, request credentials, and personal data.

Activity

  1. added
    bugSomething isn't working
    cliCLI, config inject, packaging flags
    platformOS/service/tray/ACL (Windows-heavy, not Windows-only)
    serviceService lifecycle (WinSW/launchd/scheduler)
    on Aug 11, 2026
  2. Ingwannu commented on Aug 11, 2026

    @Ingwannu
    Owner

    Confirmed against current dev (e63de8d46). The ordering is real: installServiceSafely() calls prepareServiceInstall() before installWindows(), so the existing managers and standalone proxy can be stopped before the first schtasks /create discovers that elevation is unavailable. Proxy shutdown then removes the managed Codex routing as part of its normal cleanup.

    I will not add a generic “must be elevated” early return, because the Dashboard currently relies on the structured create-access-denied result to request UAC and finalize the scheduler registration. I will change the Windows scheduler install into an explicit two-phase transaction instead:

    1. For a fresh scheduler registration, build only a unique ephemeral task XML and request the fixed Task Scheduler registration before prepareServiceInstall(). This phase must not stop a manager or proxy, rewrite canonical service assets, run the task, or touch Codex routing.
    2. The elevated registration will create the task without running it. UAC cancellation/denial, launch failure, or create failure will remove the ephemeral XML and return while the existing proxy and routing are still untouched.
    3. Only after registration succeeds will the installer enter the existing destructive commit phase: stop verified managers and the standalone proxy, write the canonical scheduler assets, run the registered task, verify the serving process, and publish install state.
    4. A commit-phase failure will never be reported as success. Cleanup will remove only a task proven to have been created by this attempt; if cleanup or ownership cannot be proved, the CLI will report the residual scheduler state instead of claiming that the prior runtime was restored.
    5. The Dashboard will use the same transaction rather than depending on assets produced by a failed destructive CLI run. The existing marker-based fallback will remain only as compatibility for older/incomplete paths.

    Regression coverage will prove that direct CLI and Dashboard UAC cancellation perform zero manager/proxy-stop calls, preserve the pre-existing routing bytes, and leave no canonical scheduler asset or install-state write. A successful path will assert the order register (not run) -> cleanup -> asset publish -> run -> verify/state.

    I am intentionally keeping existing/conflicting-manager recovery fail-closed unless the previous registration can be identified and restored exactly; I will not broaden this into an unsafe best-effort rollback.

  3. Ingwannu commented on Aug 11, 2026

    @Ingwannu
    Owner

    Implemented the verified two-phase fix in draft PR #1465:

    #1465

    The fresh Windows scheduler path now registers but does not run the owned task before any manager/proxy teardown. It then verifies the live Task Scheduler XML and only enters cleanup, canonical asset publication, task start, and install-state publication after registration succeeds. UAC cancellation or denial therefore returns before proxy shutdown or managed Codex routing removal. Existing/conflicting registrations remain on the conservative fail-closed path.

    Verification on exact dev base 87e3ff9f6:

    • 18 Windows service/UAC and adjacent CLI test files: 406 pass, 0 fail
    • TypeScript typecheck: passed
    • Privacy scan: passed
    • Documentation build: 221 pages built
    • Full suite before the final upstream rebase: 10,947 pass, 11 skip, 1 known baseline failure (Codex autostart shim > Unix shim exports persisted service API token before running Codex), also reproduced on clean dev; exact-head focused tests and all other gates above were rerun after the rebase

    The PR remains draft and the issue remains open pending independent security review and a real Windows/UAC smoke test. Review has been requested from @lidge-jun and @Wibias.

  4. lidge-jun commented on Aug 11, 2026

    @lidge-jun
    Owner

    Fixed on dev (1fa020a90 + 6312aef83, via #1465).

    ocx service install on Windows now registers the scheduler task before any destructive step. A denied UAC prompt leaves the running proxy and the injected Codex routing untouched, so a failed install can no longer break a working setup — verified against the code rather than the CI badge: no manager stop, no canonical asset write, no routing mutation, and no install-state publication happens before registration succeeds.

    Rollback additionally proves ownership before deleting: an attempt nonce is carried in the task XML and checked immediately before deletion, and where ownership cannot be proven the CLI reports the residual scheduler state rather than claiming the prior runtime was restored — which is what this thread asked for.

    One limitation worth stating: the ownership query and the elevated delete are not a single atomic operation, so a task registered in that narrow window can still be removed. Closing that properly needs an attempt-unique task identity and a real Windows host to validate; windows N/4 is dispatch-only (#1059), so no real schtasks behavior was exercised here.

    Verified: 137 pass / 0 fail across the service and elevation suites.

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingcliCLI, config inject, packaging flagsplatformOS/service/tray/ACL (Windows-heavy, not Windows-only)serviceService lifecycle (WinSW/launchd/scheduler)

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions