Skip to content

Install Codex on Linux too, from the ansible role - #17

Merged
ldayton merged 4 commits into
mainfrom
codex-ansible
Aug 22, 2026
Merged

ldayton merged 4 commits into
mainfrom
codex-ansible

Conversation

@ldayton

@ldayton ldayton commented Aug 22, 2026

Copy link
Copy Markdown
Owner

The Brewfile has had Codex since it was added, but inside if OS.mac? — Homebrew publishes only a cask for it, and casks are macOS-only. Linux was left with a comment telling you to run npm yourself, which meant a freshly provisioned box did not have it.

So the role installs it, the way it already installs Claude Code: as the account, followed by a task that runs the thing and checks it answers. npm is the route OpenAI document beside the cask and it ships the same version, so this is the same tool arriving another way.

Which npm matters, and it is named rather than found. The argument for a global install being safe here is entirely the account's own Homebrew node — global means the prefix, and the account owns the prefix outright — so the install passes executable: {{ dev_user_brew_prefix }}/bin/npm, and a stat decides whether there is one to pass. A box whose prefix has no npm is left alone. Letting the PATH choose instead is what the executed-role job caught: it runs with dev_user_brewfile: false, the runner's own npm took the install, the package landed in a prefix somewhere else, and the check went looking in the Homebrew one and found nothing.

The check needs the PATH for a reason the install does not: what npm links into the prefix is a script whose shebang is /usr/bin/env node, so running it needs node found the same way. Claude Code's check escapes that only because its installer leaves a native binary behind — copying its shape is what produced env: 'node': No such file or directory on a box where the install had just succeeded.

dev_user_codex_version pins a release for the reason dev_user_claude_code_channel does; empty takes the latest. ansible_env became ansible_facts.env at all three sites while here, since the injection behind the old name goes away in ansible-core 2.24 and every run of this role warns about it.

Run against a Linux host — the Loom CI runner, brew prefix owned by the account — both ways: with the prefix, ok=4 and changed=0 on a second pass, codex-cli 0.149.0 answering; with the prefix pointed at a path that has no npm, both tasks skip and nothing fails. The executed-role job takes the second of those, so it proves the pair is harmless on a Brewfile-less box rather than proving the install; that half has been shown only on the host above.

https://claude.ai/code/session_01WtqyKhgRvif4j6BMzZMTkA

The Brewfile has had Codex since it was added, but inside `if OS.mac?` -- Homebrew publishes only a cask for it, and casks are macOS-only. Linux was left with a comment telling you to run npm yourself, which meant a freshly provisioned box did not have it.

So the role installs it, the way it already installs Claude Code: as the account, followed by a task that runs the thing and checks it answers. npm is the route OpenAI document beside the cask and it ships the same version, so this is the same tool arriving another way. Global rather than into the home, because with the account's own Homebrew node that means the prefix, which the account owns outright -- and it is why this leans on the Brewfile, since npm arrives with node.

dev_user_codex_version pins a release for the reason dev_user_claude_code_channel does; empty takes the latest.

Claude-Session: https://claude.ai/code/session_01WtqyKhgRvif4j6BMzZMTkA
The install task carries a PATH because npm is in the prefix and no login shell has run. The check
below it needs one for a different reason: what npm links into the prefix is a script whose shebang
is `/usr/bin/env node`, so running it needs node found the same way. Written to the shape of the
Claude Code check above, which gets away without a PATH because its installer leaves a native binary
behind, it failed with `env: 'node': No such file or directory` and rc 127 on a box where the
install had just succeeded — the verification working as intended, against itself.

Found by running these two tasks on a Linux host, which is what the PR said it could not do.

Claude-Session: https://claude.ai/code/session_01RnyoBGq5XfivVqbEWMzrmE
@ldayton

ldayton commented Aug 22, 2026

Copy link
Copy Markdown
Owner Author

Ran these two tasks on a Linux host (the Loom CI runner, brew prefix owned by the account) and the check task failed where the install succeeded:

TASK [Codex answers]
fatal: [localhost]: FAILED! => {"cmd": ["/home/linuxbrew/.linuxbrew/bin/codex", "--version"], "rc": 127,
  "stderr": "env: 'node': No such file or directory"}

What npm links into the prefix is a script with a /usr/bin/env node shebang, so running it needs node on the PATH the same way the install needs npm on it. The Claude Code check above gets away without one because its installer leaves a native binary. d3fa167 adds the environment: block to the check; with it the role is ok=3 changed=0 on a second run, and codex-cli 0.149.0 answers.

https://claude.ai/code/session_01RnyoBGq5XfivVqbEWMzrmE

`ansible_env` is a top-level fact injected as a variable, and `INJECT_FACTS_AS_VARS` defaulting to
true is deprecated: the injection goes away in 2.24 and the name with it. Every run of this role
says so now, once per task that reaches for the prefix — three of them, the two that were already
here and the Codex check added by this branch. `ansible_facts.env` is the same value under the name
that stays.

Verified by running the Codex tasks on a Linux host under ansible-core 2.21.3: the deprecation
warning is gone and the role is still `ok=3 changed=0`.

Claude-Session: https://claude.ai/code/session_01RnyoBGq5XfivVqbEWMzrmE
The tasks named a prefix and trusted the PATH to lead to the npm that fills it, which is true only
where the Brewfile has run. The executed-role job runs with it off: the install found the runner's
own npm, succeeded against a prefix somewhere else, and the check went looking in the Homebrew one
and found nothing — `No such file or directory: /home/linuxbrew/.linuxbrew/bin/codex`, on a run
whose install step had just reported success.

The whole argument for this being safe is the account's own node — global means the prefix, and the
account owns the prefix — so that is the npm named now, by `executable`, and a stat decides whether
there is one. A box without the Brewfile is left alone rather than served by whatever npm the PATH
turns up into a prefix the account may not own.

Verified on a Linux host both ways: with the prefix, ok=4 and changed=0 on a second run; with it
pointed at a path that has no npm, both tasks skip and nothing fails.

Claude-Session: https://claude.ai/code/session_01RnyoBGq5XfivVqbEWMzrmE
@ldayton
ldayton merged commit 89205d7 into main Aug 22, 2026
4 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.

1 participant