Thanks to visit codestin.com
Credit goes to github.com

Skip to content

feat(install): defer app detection to first run; keep postinstall thin - #538

Merged
ndycode merged 3 commits into
mainfrom
claude/audit-20-lazy-postinstall
Jun 10, 2026
Merged

ndycode merged 3 commits into
mainfrom
claude/audit-20-lazy-postinstall

Conversation

@ndycode

@ndycode ndycode commented Jun 10, 2026

Copy link
Copy Markdown
Owner

Summary

Makes install lazy — audit roadmap §4.5.4 (docs/audits/AUDIT_2026-06-10.md, PR #522): the heavy app-bind/launcher detection moves out of postinstall into a once-only, never-throws first-run hook in the CLI, and postinstall becomes a thin CI-aware notice. This removes a whole class of install-time fragility (spawning the router, rewriting config.toml, scanning LOCALAPPDATA//Applications during npm install).

Before / after

Before After
scripts/postinstall.js 343 lines: imports dist/lib/config.js, desktop-app detection, app bind (router spawn + config.toml rewrite + startup entries), launcher shortcut install 112 lines: CI/ignored-scripts/non-TTY → silent exit 0; otherwise a two-line stderr notice. No detection, no dist imports, no node:fs, always exit 0 (invariants test-enforced)
First run ensureFirstRunSetup() in lib/runtime/first-run.ts, called from runCodexMultiAuthCli before dispatch

First-run hook design

  • Marker ~/.codex/multi-auth/first-run-setup.json: claimed with exclusive wx create (cross-process at-most-once), finalized via the repo's temp+rename atomic pattern inside withFileOperationRetry (Windows-safe).
  • Never blocks or fails a command: every step resolves to completed/skipped/failed statuses; even marker-claim failure resolves; belt-and-braces .catch at the call site. Debug-only logging, no tokens/emails.
  • All existing gates preserved: CI wins over opt-ins; CODEX_MULTI_AUTH_APP_BIND / _APP_BIND_INSTALL / _APP_LAUNCHER_INSTALL / _RUNTIME_ROTATION_PROXY honored; bind still requires a detected or already-bound desktop app. The install-context check became "module path contains node_modules", keeping dev checkouts and tests side-effect-free.

Tests + docs

  • test/first-run.test.ts (new, 15 tests): gates, once-only + marker contents, second-run skip, concurrency at-most-once, step-failure-doesn't-fail, CI skip, dev-checkout skip, CLI wiring.
  • Postinstall tests rewritten to the thin contract (silent in CI/non-TTY, notice-only on TTY, throwing sink still exits 0, no-detection source invariants).
  • README/CONFIG_FIELDS/uninstall help/preuninstall comments updated to say the bind/launcher work happens on first CLI run.

Validation

  • npm run typecheck + typecheck:scripts; eslint --max-warnings=0
  • documentation (25), codex-manager-cli (201), first-run (15), uninstall/preuninstall suites pass; the 2 remaining install-codex-auth failures are environment-only — independently re-verified byte-identical on clean origin/main
  • Manual rehearsal: CI=1 postinstall silent exit 0; TTY notice-only; dist-module first run completed→already-done; failure injection resolves without throwing; real-defaults run produced correct marker JSON

Risk / Rollback

Behavioral change to install flow (deliberate, per the audit): consumers get the bind/launcher setup on first codex-multi-auth invocation instead of during npm install. Revert the single commit to restore eager postinstall.

https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB


Generated by Claude Code

note: greptile review for oc-chatgpt-multi-auth. cite files like lib/foo.ts:123. confirm regression tests + windows concurrency/token redaction coverage.

Greptile Summary

moves desktop-app detection, Codex app bind, and launcher routing out of npm postinstall into a once-only first-run hook (lib/runtime/first-run.ts) called before CLI dispatch; postinstall becomes a thin, always-exit-0 TTY notice with no dist imports or filesystem mutations.

  • lib/runtime/first-run.ts claims a per-user marker (~/.codex/multi-auth/first-run-setup.json) with an exclusive writeFileSync(flag:"wx") for cross-process at-most-once semantics, then finalizes it atomically via withFileOperationRetry+rename; every step is best-effort and failure only debug-logs sanitized messages.
  • isInstalledPackageContext approximates the old isGlobalNpmInstall gate at CLI runtime using module path heuristics: requires a node_modules segment, excludes _npx cache runs, and excludes paths whose relative(cwd, modulePath) resolves without a .. prefix; there is an edge-case false-negative for pnpm/yarn global installs when cwd is an ancestor of the global node_modules location (e.g., running from ~/).
  • test suite covers gates, once-only semantics, concurrency, step-failure resilience, CI/dev-checkout skips, loadLauncherInstall fallback/failure modes, and CLI wiring; uses removeWithRetry per test/AGENTS.md; missing a coverage case for the "partial marker" state (claim written, process killed before finalize).

Confidence Score: 5/5

safe to merge; the change is a deliberate behavioral shift from eager postinstall to lazy first-run, and every failure path resolves gracefully without blocking any CLI command

the core once-only guarantee is backed by OS-level exclusive file creation (wx flag) and the atomic temp+rename finalize; the belt-and-braces .catch at the call site means no first-run failure can ever surface to the user; the postinstall simplification removes an entire class of install-time fragility

lib/runtime/first-run.ts — the isInstalledPackageContext CWD-relative heuristic has a false-negative for pnpm/yarn global installs when the user's working directory is an ancestor of the global node_modules path

Important Files Changed

Filename Overview
lib/runtime/first-run.ts new module implementing the lazy first-run setup hook; well-structured with deps injection, atomic marker writes via withFileOperationRetry, and belt-and-braces error handling throughout. the isInstalledPackageContext CWD-relative check has a false-negative edge case for pnpm/yarn global installs when cwd is an ancestor of the global node_modules path (e.g., running from ~/)
test/first-run.test.ts 15-test suite covering gates, once-only semantics, concurrency, step-failure resilience, CI skip, dev-checkout skip, and CLI wiring; uses removeWithRetry for Windows-safe cleanup per test/AGENTS.md; missing one coverage case for partial/incomplete marker (claim written, process killed before finalize)
scripts/postinstall.js drastically simplified from 343 to ~112 lines; removes all dist imports, desktop-app detection, and filesystem mutations; now just a CI-aware TTY notice that always exits 0
lib/codex-manager.ts adds ensureFirstRunSetup call before CLI dispatch with belt-and-braces .catch; correctly placed before loadDashboardDisplaySettings, won't block any command on failure
test/install-codex-auth.test.ts postinstall tests rewritten to the thin contract: CI-silent, TTY-notice-only, always-exit-0, and source invariants (no dist imports, no node:fs, no launcher references)
scripts/preuninstall.js comment-only update: 'postinstall bind' → 'install-time/first-run bind', no logic changes
lib/codex-manager/commands/uninstall.ts one-line help text update: 'postinstall changes' → 'first-run setup changes', no logic changes
docs/upgrade.md adds 'First-Run Setup Note' section explaining deferred setup, expected marker location, and that npx/project-local installs skip; accurate to implementation

Sequence Diagram

sequenceDiagram
    participant CLI as runCodexMultiAuthCli
    participant FRS as ensureFirstRunSetup
    participant FS as Filesystem
    participant Bind as defaultBindCodexApp
    participant Launch as defaultInstallLauncher

    CLI->>FRS: "await ensureFirstRunSetup({ notify }).catch(()=>undefined)"
    FRS->>FRS: isCiEnvironment(env)?
    alt CI / ignored-scripts
        FRS-->>CLI: "{ran:false, reason:"ci"}"
    else not installed package context
        FRS->>FRS: isInstalledPackageContext()
        FRS-->>CLI: "{ran:false, reason:"not-installed"}"
    else marker already exists
        FRS->>FS: existsSync(markerPath)
        FS-->>FRS: true
        FRS-->>CLI: "{ran:false, reason:"already-done"}"
    else first run
        FRS->>FS: "writeFileSync(markerPath, claim, {flag:"wx"})"
        Note over FS: exclusive create — at most one winner cross-process
        FRS->>Bind: bindCodexApp()
        Bind-->>FRS: "completed" | "skipped" | throws→"failed"
        FRS->>Launch: installLauncher()
        Launch-->>FRS: "completed" | "skipped" | throws→"failed"
        FRS->>FS: atomicWriteMarker (temp+rename via withFileOperationRetry)
        FRS-->>CLI: "{ran:true, appBind, launcher}"
    end
    CLI->>CLI: continue normal dispatch
Loading

Fix All in Codex

Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
lib/runtime/first-run.ts:229-233
**`isInstalledPackageContext` false-negative for pnpm/yarn global installs when cwd is a home-directory ancestor**

`relative(cwd, modulePath)` returns a path without a leading `..` whenever `cwd` is an ancestor of `modulePath`. pnpm's default global store on Linux is `~/.local/share/pnpm/global/node_modules/…`, so any user who runs `codex-multi-auth` from `~/` (or any ancestor of that path) will get `relative("~", "~/.local/…") = ".local/…"` — no leading `..`, not absolute — and the function returns `false`, silently skipping first-run setup with reason `"not-installed"`. the marker is never written, so every subsequent invocation from that cwd retries and skips again indefinitely. the same scenario applies to any configured npm prefix under the home dir (e.g., `~/.npm-global/lib/node_modules/…`). the workaround is to run from a project subdirectory, but the user has no indication anything is wrong.

### Issue 2 of 2
test/first-run.test.ts:1217-1235
**missing vitest coverage for partial/incomplete marker (claim written, process dies before finalize)**

the second-run skip test exercises a marker produced by a successful finalize. there is no test that pre-creates a marker containing only `{ version, startedAt }` (i.e., the claim was written but finalization never ran because the process was killed mid-setup) and then asserts that `ensureFirstRunSetup` returns `{ ran: false, reason: "already-done" }`. the design intention is that the claim itself is the idempotency gate, but that's an untested invariant: a test that writes the partial marker directly and calls `ensureFirstRunSetup` would pin this deliberately and guard against any future accidental "re-run on incomplete marker" logic.

Reviews (3): Last reviewed commit: "docs(upgrade): note the first-run setup ..." | Re-trigger Greptile

Before, scripts/postinstall.js was not thin: on every npm install it
loaded dist/lib/config.js, detected the packaged Codex desktop app
(LOCALAPPDATA/WindowsApps/Applications probes), auto-bound runtime
rotation (spawning the app router and rewriting config.toml), and
installed OS launcher shortcuts. Now it only does CI/ignored-scripts
and non-TTY detection (exit 0 silently) and otherwise prints a short
two-line install notice to stderr; no detection, no dist imports, no
filesystem mutation, always exit 0.

The deferred work moved to a new lazy first-run hook,
lib/runtime/first-run.ts (ensureFirstRunSetup), invoked from
runCodexMultiAuthCli ahead of command dispatch:
- marker file first-run-setup.json under the multi-auth runtime root
  (~/.codex/multi-auth), claimed with an exclusive wx create so
  concurrent first invocations run setup at most once, then finalized
  via the repo's temp+rename atomic write with withFileOperationRetry
  (Windows-safe)
- gates preserved from postinstall: CI/ignored-scripts guard wins over
  all opt-ins; CODEX_MULTI_AUTH_APP_BIND / _APP_BIND_INSTALL /
  _APP_LAUNCHER_INSTALL / _RUNTIME_ROTATION_PROXY overrides honored;
  app bind additionally requires the desktop app to be detected or
  already bound. The npm_config_global gate (meaningless at runtime)
  is replaced by an installed-package-context check (path contains a
  node_modules segment), so dev checkouts and the test suite stay
  side-effect-free
- never blocks or breaks a command: every step is try/caught, failures
  resolve to skipped/failed statuses with debug-only logging of error
  messages (no tokens/emails), and the call site adds a belt-and-braces
  .catch

Tests: postinstall suite rewritten for the thin behavior (CI silent
exit 0, non-TTY silent, TTY notice-only, throwing sink still returns 0,
no detection/dist/fs imports); new test/first-run.test.ts covers
detection + gates, runs-once marker creation, skip on second run,
at-most-once under concurrent invocations, setup failure not failing
the command, CI skip without marker, non-installed-context skip, and
CLI wiring. Docs (README, CONFIG_FIELDS) updated from
"during install/update" to "on first CLI run"; uninstall help and
preuninstall comments updated to stop claiming postinstall does the
bind/launcher work.

Note: postinstall was already CI-aware and best-effort, but not thin —
the detection/bind/launcher work genuinely ran at install time and is
what moved. Pre-existing (also on clean origin/main): 2 Windows
shortcut-routing assertions in test/install-codex-auth.test.ts fail on
Linux. Audit roadmap §4.5.4.

https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB
@chatgpt-codex-connector

Copy link
Copy Markdown

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

@coderabbitai

coderabbitai Bot commented Jun 10, 2026

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

@ndycode, we couldn't start this review because you've reached your PR review rate limit.

More reviews will be available in 6 minutes and 2 seconds. Learn how PR review limits work.

Your organization has run out of usage credits. Purchase more in the billing tab.

⌛ How to resolve this issue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans include higher PR review limits than trial, open-source, and free plans. In all cases, reviews become available again over time. During sustained high-volume PR review activity, CodeRabbit may temporarily slow when the next review becomes available.

Please see our Fair Usage Limits Policy for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: 5dc6af3b-7017-4f8e-a018-c1f40d7c1be6

📥 Commits

Reviewing files that changed from the base of the PR and between 98d9819 and 0730b9a.

📒 Files selected for processing (10)
  • README.md
  • docs/development/CONFIG_FIELDS.md
  • docs/upgrade.md
  • lib/codex-manager.ts
  • lib/codex-manager/commands/uninstall.ts
  • lib/runtime/first-run.ts
  • scripts/postinstall.js
  • scripts/preuninstall.js
  • test/first-run.test.ts
  • test/install-codex-auth.test.ts
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/audit-20-lazy-postinstall
✨ Simplify code
  • Create PR with simplified code
  • Commit simplified code in branch claude/audit-20-lazy-postinstall

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Comment thread lib/runtime/first-run.ts
Comment thread test/first-run.test.ts
Comment thread lib/runtime/first-run.ts Outdated
…oader

Review follow-ups (P1s + P2):
- isInstalledPackageContext now approximates the old npm_config_global
  gate at runtime: npx cache runs (_npx) and project-local installs
  (module under the invoking cwd) are excluded, so trying the tool via
  npx cannot mutate ~/.codex or burn the once-only marker before a real
  global install
- loadLauncherInstall gains a test-only candidate override and unit
  coverage for the fallback order, import failure, and missing-export
  cases
- test cleanup uses removeWithRetry per test/AGENTS.md instead of bare
  rmSync

https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB
Repo governance requires upgrade notes for behavior changes; the
postinstall-to-first-run move qualifies. Documents the marker path, the
npx/project-local skip, the unchanged opt-outs, and the expected
first-command outcome.

https://claude.ai/code/session_01XNtnkLbBiXZxfQQYLMpucB
@ndycode
ndycode merged commit 07e8228 into main Jun 10, 2026
1 of 2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants