Summary
runCommand in plugins/codex/scripts/lib/process.mjs:10 sets maxBuffer: options.maxBuffer. No caller except measureGitOutputBytes (lib/git.mjs:41) ever supplies a value, so for every other command that property is literally undefined.
That is not the same as omitting it. spawnSync builds its options as
options = {
__proto__: null,
maxBuffer: MAX_BUFFER, // 1 MiB
...normalizeSpawnArguments(file, args, options),
};
The spread carries an own maxBuffer property whose value is undefined, which overwrites the 1 MiB default. The result is not "1 MiB" and not "the caller's value" — it is no limit at all.
So #11 and #151 are genuinely fixed on main, but not by the size gate that PR #179 added, and not by anything named in that PR. They are fixed because a property that looks like an inert pass-through happens to delete Node's default. Nothing documents this and no test covers it.
Consequence 1: one refactor away from reopening #11 and #151
Every shape a reader would consider equivalent restores the 1 MiB cap:
maxBuffer: options.maxBuffer ?? MAX, // different
...(options.maxBuffer !== undefined && { maxBuffer: options.maxBuffer }), // different
// or just dropping the line as "it does nothing"
Same repo, same review, only that line changed:
upstream HEAD (maxBuffer: options.maxBuffer) -> OK inputMode=self-collect diffBytes=0
same logic, key omitted when unset -> ENOBUFS spawnSync git ENOBUFS
Consequence 2: the reads that are unbounded are the ones the gate never measures
PR #179's gate measures only the diff, and only to decide inline-diff vs self-collect. These run on every review, before and outside the gate, with no ceiling:
| line |
command |
git.mjs:123-125 |
diff --cached --name-only, diff --name-only, ls-files --others --exclude-standard |
git.mjs:227 |
status --short --untracked-files=all |
git.mjs:266-267 |
diff --name-only <range>, log --oneline --decorate <range> |
--untracked-files=all expands every file under every untracked directory, so a repo with an unignored node_modules/, build/ or coverage output produces megabytes of path names while the diff itself is empty. Today that is read into a JS string with no limit; after any of the refactors above it is ENOBUFS. Neither is the intended behaviour, and per #151 an ENOBUFS from binaryAvailable surfaces as the misleading "Codex CLI is not installed".
Repro
git init repro && cd repro
git commit -qm init --allow-empty
mkdir -p build/assets
python3 -c "
import os
for i in range(14000):
open('build/assets/generated_component_with_a_reasonably_long_file_name_%05d.chunk.js' % i, 'w').write('x')
"
git status --short --untracked-files=all | wc -c # 1176000 -> over Node's 1 MiB default
git diff --binary | wc -c # 0 -> the measured gate sees nothing
Then collectReviewContext(cwd, { mode: "working-tree" }) buffers ~1.15 MB of path names on main, and throws ENOBUFS under any of the equivalent-looking rewrites.
Suggested fix
Give the pass-through an explicit, generous default so the ceiling is stated rather than inherited from a spread quirk:
// Node defaults maxBuffer to 1 MiB, which ENOBUFS on any git output larger than that.
maxBuffer: options.maxBuffer ?? 256 * 1024 * 1024,
measureGitOutputBytes still passes its own small bound, so the inline-diff / self-collect gate is unaffected. A regression test asserting that a >1 MiB git status survives runCommand would pin the behaviour that #11 and #151 currently depend on implicitly.
Environment
- codex-plugin-cc:
main @ db52e28 (1.0.6); originally hit on 1.0.2, which had no maxBuffer line at all
- Node.js: v24.12.0 and v25.9.0 (identical results)
- macOS arm64
Summary
runCommandinplugins/codex/scripts/lib/process.mjs:10setsmaxBuffer: options.maxBuffer. No caller exceptmeasureGitOutputBytes(lib/git.mjs:41) ever supplies a value, so for every other command that property is literallyundefined.That is not the same as omitting it.
spawnSyncbuilds its options asThe spread carries an own
maxBufferproperty whose value isundefined, which overwrites the 1 MiB default. The result is not "1 MiB" and not "the caller's value" — it is no limit at all.So #11 and #151 are genuinely fixed on
main, but not by the size gate that PR #179 added, and not by anything named in that PR. They are fixed because a property that looks like an inert pass-through happens to delete Node's default. Nothing documents this and no test covers it.Consequence 1: one refactor away from reopening #11 and #151
Every shape a reader would consider equivalent restores the 1 MiB cap:
Same repo, same review, only that line changed:
Consequence 2: the reads that are unbounded are the ones the gate never measures
PR #179's gate measures only the diff, and only to decide
inline-diffvsself-collect. These run on every review, before and outside the gate, with no ceiling:git.mjs:123-125diff --cached --name-only,diff --name-only,ls-files --others --exclude-standardgit.mjs:227status --short --untracked-files=allgit.mjs:266-267diff --name-only <range>,log --oneline --decorate <range>--untracked-files=allexpands every file under every untracked directory, so a repo with an unignorednode_modules/,build/or coverage output produces megabytes of path names while the diff itself is empty. Today that is read into a JS string with no limit; after any of the refactors above it isENOBUFS. Neither is the intended behaviour, and per #151 anENOBUFSfrombinaryAvailablesurfaces as the misleading "Codex CLI is not installed".Repro
Then
collectReviewContext(cwd, { mode: "working-tree" })buffers ~1.15 MB of path names onmain, and throwsENOBUFSunder any of the equivalent-looking rewrites.Suggested fix
Give the pass-through an explicit, generous default so the ceiling is stated rather than inherited from a spread quirk:
measureGitOutputBytesstill passes its own small bound, so theinline-diff/self-collectgate is unaffected. A regression test asserting that a >1 MiBgit statussurvivesrunCommandwould pin the behaviour that #11 and #151 currently depend on implicitly.Environment
main@ db52e28 (1.0.6); originally hit on 1.0.2, which had nomaxBufferline at all