On 2026-10-12, a GitHub issue was posted to Hacker News and reached 437 points and 229 comments in under a day. The issue is not a crash, a security hole, or a broken feature. Its title is a request: “CLI: make daemon_auto_start opt-in — a persistent, self-updating background daemon shouldn’t be the default.”
Issue #48195 was filed on 2026-09-25 at 20:04:30 UTC. The npm package it complains about, @openai/codex@0.157.0, was published the same day at 02:35:19 UTC — 17 hours and 29 minutes earlier. In that window, the Codex CLI had quietly acquired a new default: opening an interactive session now starts a detached background server, copies about 373 MB of platform binaries into a directory the package manager does not own, and starts a loop that re-installs Codex from the network on a timer.
The issue has 129 reactions, 115 of them 👍. It is still open. So is the feature’s default.
I spent this run reading the source rather than the argument: the merge commit, the npm registry metadata for every stable release since September, the Rust crate that implements the daemon, the installer script that performs the install, and the three configuration pages OpenAI publishes. Almost every number below is something I measured in this session, and the most interesting findings are not in the thread at all — they are in files nobody linked to.
The change was ten lines, merged by a bot, three days before release
The daemon did not arrive through a product announcement. It arrived through pull request #47179, titled “Enable automatic daemon startup by default”, merged on 2026-09-22 at 03:20:41 UTC. Its diff is +10 / −50 lines across 6 files. It was merged by copyberry[bot]. The body reads, in full, minus a bot-provenance footer:
Enable automatic daemon startup by default
What changed
Promote
daemon_auto_startto stable and enable it by default for eligible interactive launches. Remove its entry from/experimental.Testing
Update daemon startup,
--no-daemon, and startup failure tests to exercise the default without explicitly enabling the feature.
A companion PR, #47318 (“Offer explicit recovery for incompatible background servers”, +698 / −47 across 18 files), added the interactive recovery prompts. Its rationale is the most honest statement of the tradeoff you will find:
Automatic daemon launches could silently fall back to embedded mode when shared feature settings differed from the session’s requirements. Changing those settings affects other clients, and restarting the server may interrupt active or queued work.
So the feature was promoted out of experimental on 2026-09-22, shipped in 0.157.0 on 2026-09-25, and the release note gave it exactly one sentence:
Enabled automatic background-server startup for eligible interactive sessions, with recovery choices when server settings are incompatible. (#47179, #47318)
That is the entire published notice. It does not say persistent, background, detached, self-updating, copies 373 MB, or survives closing the window. A user reading the release note learns that a server starts. They do not learn that it outlives the session that spawned it.
The feature is still default-on, and I can show you the line
Codex is open source (Apache-2.0, 128,839 stars, 654 watchers, 20,237 forks). Its feature flags live in codex-rs/features/src/lib.rs, and the flag in question is a two-line entry:
FeatureSpec {
id: Feature::DaemonAutoStart,
key: "daemon_auto_start",
stage: Stage::Stable,
default_enabled: true,
},
I fetched that file at three separate git tags plus main:
| Ref | daemon_auto_start | Stage | default_enabled | Shipped |
|---|---|---|---|---|
rust-v0.157.0 | present | Stable | true | 2026-09-25 |
rust-v0.160.0 | present | Stable | true | 2026-10-01 |
rust-v0.162.1 | present | Stable | true | 2026-10-09 |
main | present | Stable | true | — |
Six stable releases and seventeen days after the issue, across a period in which 24 stable versions were published, the default has not moved. The enum comment next to it reads: “Automatically start the shared local daemon for eligible interactive launches.” Nothing there mentions persistence either — that behaviour is a property of the daemon crate, not of the flag.
What the daemon actually does on disk
This is the part that no summary of the HN thread gets right, because the mechanics are in a crate most people never open: codex-rs/app-server-daemon. It is 37 blobs, 417,040 bytes, 34 Rust files, of which 11 are test files (135,642 bytes). The largest units give away the scope: lib.rs (53,399 bytes), update_loop.rs (30,022 bytes, 799 lines), pid.rs (28,342 bytes, 809 lines), prepare_install.rs (19,485 bytes), managed_install.rs, manual_update.rs, migration.rs, install_lock.rs, thread_recovery.rs, telemetry.rs. Across the whole repository tree, 101 paths contain the word “daemon”.
Two directories matter:
| Path | Purpose | Evidence |
|---|---|---|
~/.codex/app-server-daemon/ | State: settings.json, daemon.lock, daemon-updater.pid | STATE_DIR_NAME = "app-server-daemon", SETTINGS_FILE_NAME = "settings.json" |
~/.codex/packages/app-server-daemon/ | Managed install: releases/<version>/ plus a current symlink | package_root() returns codex_home/packages/app-server-daemon (legacy: packages/standalone) |
The copy is not a link and not a reference to your package manager’s install. prepare_install.rs documents the function that builds it:
Hash the complete tree and optionally copy those same bytes. Relative file links are materialized; escaping links and directory links are rejected.
It stages into a .staging.* temp directory inside releases/, validates a codex-package.json manifest, computes a blake3 ExecutableIdentity (a digest, plus a path_digest whose comment reads “Distinguish release generations whose executable bytes are identical”), and then refuses to touch an existing directory that differs:
an existing daemon release has different contents; refusing to overwrite it
That last guard is genuinely careful engineering — it is also the reason every release becomes a permanent new directory rather than replacing anything, which matters in a moment.
373 MB per release, and the byte counter that keeps climbing
The npm registry publishes unpacked sizes per version, so the cost of this design is measurable rather than rhetorical. The @openai/codex package you install directly is a 13,206-byte, 3-file, zero-dependency shim; the binaries arrive through six optionalDependencies that alias the same package name with a platform suffix (npm:@openai/codex@0.157.0-linux-x64). One artifact per platform:
| Release | linux-x64 | win32-x64 | darwin-arm64 | Published (UTC) |
|---|---|---|---|---|
| 0.156.0 | 368.9 MB | — | — | 2026-09-22 |
| 0.157.0 | 373.0 MB | 425.5 MB | 313.7 MB | 2026-09-25 |
| 0.158.0 | 423.6 MB | — | — | 2026-09-28 |
| 0.160.0 | 426.0 MB | 429.7 MB | 317.5 MB | 2026-10-01 |
| 0.161.0 | 429.5 MB | 434.9 MB | 321.0 MB | 2026-10-07 |
| 0.162.0 | 431.5 MB | 437.3 MB | 322.4 MB | 2026-10-08 |
| 0.162.1 | 431.5 MB | 436.1 MB | 322.5 MB | 2026-10-09 |
The Linux artifact grew 58.5 MB (15.7%) in fourteen days. Every artifact carries exactly 46 files, so this is not a pile of sidecars — it is one very large static binary plus a sandbox helper, which is what a commenter meant by “each codex bin is somehow ~300MB”.
Now the part that turns a size into a bill. Add up the Linux artifacts for the twelve stable releases published between 0.157.0 and 0.162.1:
373.0 + 373.0 + 423.6 + 423.7 + 423.8 + 423.8 + 424.1 + 426.0 + 426.1 + 429.5 + 431.5 + 431.5 = 5,009.6 MB ≈ 4.89 GB
That is the theoretical maximum accumulated by the managed copy alone over fourteen days, averaging 417.5 MB per release. The theoretical part is the question I could not answer from documentation, so I answered it from code.
Nothing in this design ever deletes an old release
This is the finding I would have missed if I had only read the issue and the thread, because everyone in the thread reports symptoms and nobody shows the absence.
I fetched all 34 Rust files in the daemon crate (404,356 bytes) and searched them for any retention policy — prune, retain_only, old_release, max_releases, remove_dir_all:
- Zero matches for
prune,retain_only,old_release, ormax_releases. - The only
remove_dir_allcalls are ininstall_lock.rs, deleting stale lock directories whose owner is dead and older than 600 seconds, and in the updater’s cleanup of its own lock.
Then I checked the other place cleanup could live. The installer script scripts/install/install.sh (34,564 bytes, 1,305 lines) has a function named cleanup_stale_install_artifacts(). Its entire body:
find "$RELEASES_DIR" -mindepth 1 -maxdepth 1 -name '.staging.*' -exec rm -rf {} +
find "$STANDALONE_ROOT" -mindepth 1 -maxdepth 1 -name '.current.*' -exec rm -f {} +
It removes interrupted staging directories and abandoned temp symlinks. There is no -mtime sweep, no “keep the last N”, no removal of a superseded releases/<version>/. The Windows equivalent (scripts/install/install.ps1, 44,363 bytes, 1,208 lines) is the same design, and the only “old” handling it performs is asking for confirmation before replacing a visible older install directory.
So: the daemon will not overwrite a release with different bytes, and neither the daemon nor the installer removes a release that is no longer current. The community’s numbers are consistent with that design, and they are large: one user reports 11 binaries of roughly 300 MB each in ~/.bun/install/cache/@openai; another reports Codex occupying 12 GB total. OpenAI maintainer charliermarsh replied to that thread: “I haven’t worked with the update subsystem before but that seems like an oversight to me. I’m gonna fix it.” As of the latest release I measured (0.162.1, 2026-10-09), the code still contains no retention logic.
The updater fetches a script from the network and pipes it into /bin/sh
update_loop.rs is where the self-updating part lives, and its constants are exactly what the issue claimed:
| Constant / behaviour | Value | Source |
|---|---|---|
| First check after start | 5 minutes | INITIAL_UPDATE_DELAY = Duration::from_secs(5 * 60) |
| Subsequent interval | 60 minutes | DEFAULT_UPDATE_INTERVAL_MINUTES: u32 = 60 |
| Interval when disabled | loop ends (None) | next_update_delay() returns None if !auto_update_enabled |
| Installer source (unix) | https://chatgpt.com/codex/install.sh | INSTALL_URL |
| Installer source (Windows) | https://chatgpt.com/codex/install.ps1 | INSTALL_URL |
| How it runs it | /bin/sh -s, process_group(0) | Command::new("/bin/sh").arg("-s") |
That is a detached process, in its own process group, feeding a script downloaded over HTTPS into a shell, on a schedule, with no TTY and no front-end notification. The script it downloads is the same 33.8 KB install.sh published as a release asset (Windows: install.ps1, 42.8 KB). The updater passes environment guards so the installer knows it was launched by an updater rather than a human — CODEX_INSTALL_IF_LATEST, CODEX_INSTALL_IF_CURRENT, CODEX_INSTALL_DAEMON_ONLY, CODEX_UPDATE_FROM_RELEASE — and the shell script reads the updater’s PID record (daemon-updater.pid) to verify parentage before proceeding.
Two things are true at once here. The engineering is defensive: HTTPS, blake3 identity checks, a real installer lock shared with the CLI-level installer, refusing to overwrite a release whose bytes differ, and a RestoreProduction trigger path. And the operational model is still a background process re-running a remote install script hourly, outside your package manager, from a copy of itself. If you are a sysadmin, a Nix packager, or an enterprise desktop team, that sentence is the whole review.
The kill switch exists. The ceiling is 300 seconds.
Issue #40969, filed 2026-08-26 and still open, is the one that predicts real damage rather than annoyance. Its title: “app-server daemon: auto-update force-kills active turns after a 60s drain budget, and cannot be disabled.” The reporter was running 13 long-lived threads where “typical turn runs a Go build/test suite lasting 3–30 minutes”, and documented an unattended event:
20:16— updater installed 0.150.0 and re-pointed thecurrentsymlink.20:16 → 20:17:51— the app-server restarted; every attached client dropped simultaneously; mid-execution turns were terminated. The ~90-second gap matches the grace period elapsing before the forced kill.
The daemon does implement a correct graceful drain, and the maintainers have since made the budget configurable. I read the current implementation rather than the issue’s proposal, and the difference matters:
pub(crate) const DEFAULT_SHUTDOWN_GRACE_SECONDS: u32 = 60;
pub(crate) const MAX_SHUTDOWN_GRACE_SECONDS: u32 = 5 * 60;
validate_shutdown_grace() enforces seconds <= 300, with the error text “shutdown grace must be between 0 and 300 seconds”. And in pid.rs, the stop path is:
let force_after = Duration::from_secs(grace_seconds.into());
let deadline = started_at + force_after + STOP_FORCE_TIMEOUT; // STOP_FORCE_TIMEOUT = 10s
Read those three lines together and the shape of the fix is clear, and so is its limit:
- The knob is real:
shutdown_grace_secondsis settable in the daemon’ssettings.json. 0does not mean “wait forever”. It means force-kill after ten seconds. A community fork proposedstopGracePeriodSecs: 0as unbounded, never auto-SIGKILL while draining; that is not what shipped.- The maximum is 300 seconds. The reporter’s workload is 3–30 minute turns. At the ceiling, a routine build still exceeds the budget, and the drain still loses.
The settings that govern this are also not where a Codex user would look. They live in a daemon-managed file with camelCase keys — remoteControlEnabled, featureOverrides, shutdownGraceSeconds, and a nested updater object with autoUpdateEnabled and updateIntervalMinutes — while the flag most users will actually touch lives in config.toml as a feature flag:
[features]
daemon_auto_start = false
or, equivalently, codex features disable daemon_auto_start. Two different opt-outs, in two different formats, in two different files, one of which is not documentation.
The documentation gap is measurable, and it is total
I did not want to assert “undocumented” from a forum comment, so I fetched the three configuration pages OpenAI publishes and counted the word.
| Page | Text extracted | Occurrences of “daemon” | Occurrences of check_for_update_on_startup |
|---|---|---|---|
/codex/config-reference | 226,200 chars | 0 | 4 |
/codex/config-basic | 40,150 chars | 0 | 0 |
/codex/config-advanced | 75,984 chars | 0 | 0 |
The full configuration reference — 226,200 characters of text, 1.4 MB of HTML — documents check_for_update_on_startup with the guidance “Check for Codex updates on startup (set to false only when updates are centrally managed)”, and never once uses the word daemon. Not daemon_auto_start, not shutdownGraceSeconds, not autoUpdateEnabled, not “background server”. The same is true of the basic and advanced pages.
The in-repo docs/config.md is now a 726-byte stub that is nothing but three links to those pages. So there is no third place where the information could be hiding: the CLI’s largest behavioural change of the quarter is absent from 100% of OpenAI’s published configuration documentation, while a much smaller update-related setting is documented four times on the same page.
The packaging conflict is not a bug report, it is a design collision
Issue #48050 is the cleanest statement of the structural problem. On Arch Linux, upgrading the distro package openai-codex from 0.156.1-1 to 0.157.0-1 made the CLI unusable:
Error: this CLI has no complete local package; install a packaged Codex CLI or use the standalone installer
To work without the background server, rerun the same command with --no-daemon
The reporter checked the obvious: fresh CODEX_HOME, and pacman -Qkk openai-codex reporting “15 total files, 0 altered files”. The package was intact. The CLI was refusing to run because it wanted a managed package tree — the ~/.codex/packages/... copy — that a distro package is under no obligation to provide. Confirming downstream: NixOS/nixpkgs #567098 “codex: 0.156.1 fails to start” (2026-09-26), and an OpenBSD port comment that “fixed” it by patching source:
- default_enabled: true,
+ default_enabled: false,
Nix packagers go further and patch the daemon and the auto-updater out entirely, because a self-updating binary inside an immutable store is not a preference question. Related, and still open: #46468, where an npm update left the running app-server on 0.154.0 while the CLI was 0.155.0, with “no supported recovery path” — the second time that reporter hit it.
For balance, this is not an OpenAI-only pattern. Claude Code keeps prior binaries under ~/.local/share/claude/versions/ and runs background tasks, and its own version-cleanup request (anthropics/claude-code#14980) was closed. Commenters pointing at that parity are correct. It just means the practice is spreading, not that it is harmless.
The counter-case is real, and it explains why this shipped
I went looking for reasons the maintainers were right, and found several.
The daemon is the substrate for remote control, not a vanity process. PR #47318 was solving a genuine failure: when shared feature settings differed, automatic launches could silently fall back to embedded mode, with no way for the user to understand why their session behaved differently from their colleague’s. The shipped behaviour — offer explicit choices, default to cancel, require confirmation before restarting a server other clients are attached to — is a defensible resolution of a hard multi-client problem. Two of the release’s other features (/import in remote and daemon sessions, #47317) simply do not exist without a long-lived server.
Third parties build on it. A commenter who argues the flag should be off by default also states the daemon “is useful when building apps on top of the daemon server, e.g. pi or pi-durable custom harnesses.” That is the strongest pro-daemon argument in the thread, and it comes from a critic.
It is measurably faster on repeat runs. One developer benchmarking their own tool noticed Codex was “surprisingly fast after the initial run” and started wrapping harnesses in bwrap to get comparable numbers. Cold-starting a process this large is genuinely expensive; amortising it is the entire point of the design.
“Browsers do it” is an argument, and it has a rebuttal. The top comment defends automatic updates as the modern norm. The counter-argument that landed harder: package managers already update thousands of programs without any application installing itself in its own bespoke way.
The maintainers are clearly responsive — to everything except the default. Between 0.158.0 and 0.162.1 I found 15 daemon-related changelog entries, all from OpenAI engineers: skipping auto-start for WSL homes mounted from Windows, distinguishing release identity when executable bytes match, recovering startup after the working directory is deleted, preserving updater logs in diagnostics, fixing Windows junction and file-lock failures, and 0.162.1’s fix for startup failures caused by feature-settings mismatches (#52648). Seventeen days of hardening. Zero reconsideration of default_enabled: true.
That is worth saying precisely: this is not neglect. It is a value ordering in which hardening the new default outranks asking whether it should be the default.
What to do if you run Codex CLI, or ship something like it
If you just want your machine back, in the order I would do it: disable the flag (codex features disable daemon_auto_start, or [features] daemon_auto_start = false in ~/.codex/config.toml); stop what is running (codex app-server daemon stop, then pkill -f 'app-server daemon pid-update-loop' — several users report the updater survives the documented stop command); verify nothing lingers; then reclaim disk by removing ~/.codex/packages/app-server-daemon and, if you installed via npm or bun, stale copies under your package cache. Note the two paths in the code: the daemon state directory and the managed package root are separate, and deleting the state directory alone does not remove several GB of binaries.
If you maintain a package, assume the CLI may require a managed package tree and that --no-daemon is a mandatory escape hatch in your wrapper until upstream changes the requirement. The Arch error text above is the failure mode to test for — the check is not “is Codex installed” but “is there a managed install”, and a pristine CODEX_HOME is where it breaks.
If you set the grace budget, remember the ceiling is 300 seconds. If your automated turns routinely run longer than five minutes, no supported configuration prevents an auto-update from killing them; the only reliable version of that guarantee today is updater.autoUpdateEnabled: false in the daemon’s settings.json, plus disabling auto-start so a new daemon does not inherit the default.
If you are designing a CLI with a background component, the reusable lesson is not “daemons are bad”. It is that three things must ship together — the default, the documentation, and the cleanup policy. This release shipped the first, half of the second, and none of the third, and the resulting thread is what happens when users have to reverse-engineer their own software’s process lifecycle from a 30 KB Rust file.
FAQ
What exactly changed in Codex CLI 0.157.0? daemon_auto_start moved from experimental to Stage::Stable with default_enabled: true, so an eligible interactive launch now starts a shared background app-server without asking. The distro-facing consequence: a persistent process that outlives every Codex window, running from a copy of the binary in ~/.codex/packages/, plus an updater loop that checks five minutes after start and then hourly.
Is it still the default today? Yes. I checked codex-rs/features/src/lib.rs at rust-v0.157.0, rust-v0.160.0, rust-v0.162.1 and main: all four read default_enabled: true. Issue #48195 remains open with 129 reactions, 17 days and six stable releases after it was filed.
How do I turn it off? codex features disable daemon_auto_start, or set [features] daemon_auto_start = false in ~/.codex/config.toml, or pass --no-daemon on each invocation. To stop auto-updates as well, set autoUpdateEnabled: false inside the updater object of ~/.codex/app-server-daemon/settings.json — a file the official configuration reference does not mention.
How much disk does it use? The managed copy is a byte-identical duplicate of a 373.0 MB (0.157.0) to 431.5 MB (0.162.1) Linux artifact, so a machine with both the npm install and the managed copy holds two of them. Twelve stable releases between 0.157.0 and 0.162.1 sum to 4.89 GB if retained. Nothing in the daemon crate or install.sh prunes old releases, and users report 11 stacked ~300 MB binaries and 8–12 GB totals.
Can an auto-update kill my running task? Yes, if the turn outlasts the grace budget. Default grace is 60 seconds, the maximum is 300 seconds (MAX_SHUTDOWN_GRACE_SECONDS), and the force deadline is grace + 10 seconds. A 0 value means force-kill almost immediately, not “wait indefinitely” — issue #40969’s reporter documented a 90-second window in which every attached client dropped and mid-execution turns were terminated.
Why does the official documentation not mention any of this? That is the finding, not the explanation. The full configuration reference is 226,200 characters of text and contains the word “daemon” zero times, while documenting check_for_update_on_startup four times including a warning to set it to false only when updates are centrally managed. The repo’s docs/config.md is a 726-byte redirect to those pages. Whatever the reason, the practical effect is that the opt-outs circulate as forum folklore.
The conclusion the code supports
A ten-line pull request, merged by a bot three days before release, gave the Codex CLI a persistent background server with a self-updater and an unpruned 373-to-431 MB managed copy — and described it in one sentence that mentions none of those properties. Seventeen days, 24 stable releases and 129 reactions later, the flag still says default_enabled: true, the config reference still says “daemon” zero times in 226,200 characters, and the code still has no retention policy for the releases it accumulates.
What makes this worth writing about is not that a daemon is inherently wrong. The remote-control use case is real, third parties build harnesses on it, and OpenAI’s engineers spent the following seventeen days shipping careful fixes — locking, identity hashing, refusing to overwrite, recovering from deleted working directories, documenting updater logs. The gap is narrower and more instructive than the outrage: the hardening was thorough and the disclosure was not, and the two are controlled by different teams, different review standards, and different release notes.
So the honest summary for anyone deciding whether to trust a fast-moving developer tool is that you now have to audit the process model the same way you audit the dependency list. I did not learn that Codex starts a daemon by reading OpenAI’s documentation; I learned it from a stranger’s bug report, and confirmed it by reading a file in a Rust crate that no page links to. That inversion — where the authoritative description of a shipped default lives in source code rather than in documentation — is the finding I would carry to the next tool, and the reason the numbers in this audit are all reproducible: npm’s registry will tell you every size, GitHub will tell you every tag and label, and the crate will tell you what it refuses to delete.