Skip to content

fix(hooks): make Codex hook commands portable shell-free argv (#1215) - #1231

Merged
SaulMoro merged 8 commits into
Tencent:mainfrom
kittimzhe:fix/portable-hook-command-1215
Oct 22, 2026
Merged

SaulMoro merged 8 commits into
Tencent:mainfrom
kittimzhe:fix/portable-hook-command-1215

Conversation

@kittimzhe

Copy link
Copy Markdown
Contributor

Fixes #1215

Summary

Codex runs command through the host shell on every platform and never reads commandWindows — HookHandlerConfig::Command has no such field, and serde drops the key silently. Every shell-wrapped Codex entry has therefore been a ParserError on Windows: built-ins and team dispatcher alike, no matter what hooks inject reported.

Change

  • Built-ins become the bare argv form teamai hook-dispatch <E> --tool codex: no shell syntax to parse, so PowerShell and POSIX sh run it identically; byte-identical across platforms, which keeps the [bug] Codex hook entries on Windows are rendered in bash syntax and silently never run #1074 project-file contract — now without the dead commandWindows field. Fail-open is not a shell tail: the dispatcher's unconditional finally { process.exit(0) } (index.ts) already covers built-in events, so 2>/dev/null || true and 2>$null; exit 0 were belt-and-suspenders at the command layer.
  • Team dispatcher becomes the absolute wrapper path "<home>/.teamai/bin/teamai[.cmd]" hook-dispatch <E> --tool codex --team-hooks — the self-contained option from the report: shell-free (double quotes parse identically in both shells), and no PATH assumption, since the wrapper directory is exactly what a restricted-PATH host misses. The dispatcher only ever lives in the per-machine user-level ~/.codex/hooks.json, so the absolute path breaks no cross-platform byte-stability contract. reconcileCodexDispatchers calls ensureTeamaiWrapper() before writing; if the wrapper can't be created it falls back to the bare name, which still parses everywhere.
  • DISPATCHER_COMMAND now matches both the legacy shell-wrapped forms (with or without a Git Bash launcher) and the new absolute form, so entries written by earlier versions are recognized and replaced on the next inject — verified below.

Verification records (real CLI, seeded HOME)

Seeded ~/.codex/hooks.json with the exact legacy shapes from the report (bash dispatcher, bash built-in with commandWindows, plus a user's own entry), then teamai init + hooks inject:

  • Built-ins rewritten to teamai hook-dispatch <E> --tool codex — bare, no commandWindows, per-hook timeouts preserved (Skill/TodoWrite matchers intact).
  • The dispatcher entry rewritten to "<home>/.teamai/bin/teamai" hook-dispatch Stop --tool codex --team-hooks with the largest team-hook timeout (45s), placed before the built-in — and the wrapper file exists at the path the command names.
  • The user's own echo user hook entry left byte-identical ([bug] Delivery and local-state bugs on main found while scoping #915 #993).

Tests

Red-first, all green (vitest run, 22 in the two touched suites; full unit suite passes except 3 pre-existing failures that also fail on clean main — the dsh-bridge test needs dsh absent, and this machine has it installed):

…t#1215)

Codex runs `command` through the host shell on every platform and never
reads `commandWindows` (HookHandlerConfig::Command has no such field;
serde drops it silently), so every shell-wrapped Codex entry has been a
ParserError on Windows — built-ins and team dispatcher alike.

- Built-ins become the bare argv form `teamai hook-dispatch <E> --tool
  codex`: parses identically under PowerShell and POSIX sh, stays
  byte-identical across platforms (the Tencent#1074 project-file contract, kept
  without the dead commandWindows field). Fail-open is the dispatcher's
  unconditional finally-exit-0 in index.ts, not a shell tail.
- The team dispatcher entry becomes the absolute wrapper path
  `"<home>/.teamai/bin/teamai[.cmd]" hook-dispatch ...` — shell-free
  and PATH-assumption-free; the user-level ~/.codex/hooks.json is
  per-machine, so the absolute path breaks no cross-platform contract.
  reconcileCodexDispatchers ensures the wrapper exists before writing.
- DISPATCHER_COMMAND matches both the legacy shell-wrapped forms and the
  new absolute form, so existing installs migrate on the next inject.
PowerShell parses a quoted path in command position as a string
expression — ParserError on the arguments that follow — and needs the
call operator `&`; POSIX sh needs no operator and would treat `&` as
backgrounding (measured on Windows 11 / PowerShell 5.1 in the Tencent#1215
thread). The writer knows the platform, so it emits the form that
platform's host shell runs: `& "<abs teamai.cmd>" ...` on Windows,
`"<abs teamai>" ...` elsewhere.
@github-actions

Copy link
Copy Markdown
  • [P1 blocking] Preserve fail-open when the CLI cannot start — src/builtin-hooks.ts:224. If Codex’s hook environment cannot resolve teamai (for example, a restricted PATH), the new bare command exits nonzero instead of succeeding through the previous || true. The dispatcher’s finally cannot help because it never starts. Keep startup failures fail-open without reintroducing incompatible shell syntax.

  • [P2 non-blocking] Make the new tests platform-aware — src/__tests__/codex-team-hooks.test.ts:116. This assertion and the legacy-replacement assertion at src/__tests__/codex-team-hooks.test.ts:168 require a command beginning with ", but Windows now correctly emits & "…teamai.cmd". Both tests therefore fail on Windows; the fixture also creates only the extensionless wrapper. Mock POSIX explicitly or assert and seed the platform-specific form.

The PR description includes a representative real-CLI verification record, satisfying the testing requirement. No commands from the PR were executed.

…nt#1215)

The bare argv form's PATH dependency isn't new — the old bash form
needed teamai on PATH too — but its fail-open tail covered the
not-found case, and the bare form dropped that: a machine where teamai
is not on the hook PATH now erred visibly with an empty $LASTEXITCODE
(measured on Windows 11 / PowerShell 5.1 in the Tencent#1215 thread), because
the dispatcher's finally-exit-0 only covers failures after the command
resolves.

The '; exit 0' tail closes it back: PowerShell's
CommandNotFoundException is non-terminating and POSIX sh continues
after a not-found, so the entry exits 0 in both shells either way. The
tail parses in both shells, keeping the string byte-identical across
platforms (Tencent#1074). POSIX half verified on sh: not-found -> exit 0.
@kittimzhe

Copy link
Copy Markdown
Contributor Author

Landed the not-found half of the direction agreed in the thread (22e3732): every Codex built-in now ends in ; exit 0 — PowerShell's CommandNotFoundException is non-terminating and POSIX sh continues after a not-found, so a machine without teamai on the hook PATH exits 0 instead of erroring with an empty $LASTEXITCODE. The tail parses in both shells, so the string stays byte-identical across platforms (#1074). POSIX half verified on sh (not-found → exit 0); the PowerShell half is the 30-second measurement offered in the thread.

The user-level absolute-wrapper half is deliberately held until the Codex PATH question is answered — it needs scope-aware matching, which isn't worth building ahead of that fact.

@github-actions

Copy link
Copy Markdown
  • [P2 non-blocking] Windows test failures remain — src/__tests__/codex-team-hooks.test.ts:116 and src/__tests__/codex-team-hooks.test.ts:168 still require commands beginning with ", but Windows emits & "…teamai.cmd". The fixture also creates only teamai, not teamai.cmd. Mock POSIX explicitly for these tests or make their assertions and fixture platform-aware. The added Windows-specific test does not resolve this earlier finding.

The earlier fail-open finding is resolved: built-in commands now append the portable ; exit 0 tail.

The PR description includes a representative real-CLI verification record, satisfying the testing requirement. No PR code was executed, built, or installed.

@SaulMoro

Copy link
Copy Markdown
Collaborator

@kittimzhe Codex left a remaining finding on the Windows test assertions in codex-team-hooks.test.ts. Mind taking a look?

@SaulMoro

Copy link
Copy Markdown
Collaborator

@kittimzhe @natinli Codex does read commandWindows (codex-rs/config/src/hook_config.rs:144 and codex-rs/hooks/src/engine/discovery.rs:470 at rust-v0.142.5, added in openai/codex#22159), so the premise of #1215 does not hold and the binary grep was a false negative. Since the 8 entries already carried commandWindows, the Windows failure has another cause. Could you reproduce it and capture Codex's actual hook error before we go further here? If it turns out to be PATH, the absolute wrapper path may still be worth a smaller, separate change.

@SaulMoro

SaulMoro commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

This was generated by AI during triage.

Following up on #1215 (comment). The premise in this PR's title, description, comments and tests ("Codex never reads commandWindows") does not hold, and Codex runs Windows hooks through cmd.exe. Under cmd, head 22e3732 behaves like this:

Since you already plan to rework this, here is a proposed split so that two PRs do not rewrite the same entry:

  • This PR, built-ins only. Keep command as on main and restore commandWindows in cmd syntax: teamai hook-dispatch <event> --tool codex[ --matcher X] 2>nul & exit 0. Update codexWindowsCommand and its comment in src/hooks.ts. Revert the dispatcher changes. That also removes the Windows test finding from the Codex review.
  • fix(hooks): give the Codex team-hook dispatcher a Windows command (#1209) #1210, the dispatcher. It already adds the dispatcher's commandWindows.

For the e2e record:

  • One hooks inject on Windows, showing that the rewritten entries end up trusted. The new commandWindows changes the hash Codex computes for each entry.
  • A Codex run that reports the hook Completed even when teamai is missing from PATH.
  • One more cheap check. On main, cmd sends 2>$null to a stray file in the hook's working directory (named $null, or possibly $null;), so after the fix that file should stop appearing.

Please also update the title and description to match the new scope.

@SaulMoro
SaulMoro self-requested a review October 20, 2026 13:19

@SaulMoro SaulMoro left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was generated by AI during triage.

Blocking until the changes in #1231 (comment) land. The premise that Codex ignores commandWindows does not hold, so the scope needs reworking: built-ins only, with a cmd-syntax commandWindows.

…t#1215)

Root cause found in the thread: Codex runs hook commands through
%COMSPEC% /C on Windows (codex-rs hooks/src/engine/command_runner.rs,
default_shell_command), not PowerShell. The pre-Tencent#1215 field carried
PowerShell syntax, so 2>$null created a file named $null and
'; exit 0' arrived as junk arguments — Windows hooks never ran
correctly. This also reverts the '; exit 0' tail (22e3732): a semicolon
is not a cmd separator either, so the tail is cmd-hostile the same way.

Built-ins now carry the bare POSIX command (unchanged, portable) plus a
cmd.exe commandWindows: nul redirect, '&' separator, exit 0 — the
not-found fail-open under the actual hook shell. The '&' runs the exit
whatever the dispatch did, mirroring '|| true' on the POSIX side. Builds
without commandWindows support fall back to the bare command.

The dispatcher keeps the plain quoted absolute path (no '&' call
operator — that analysis answered the wrong shell): cmd.exe /C and sh
alike resolve a quoted program path, and openai/codex#33926 wraps the
command in an extra quote layer under cmd.
@kittimzhe

Copy link
Copy Markdown
Contributor Author

Reworked per the root cause found in the thread: Codex runs hooks through %COMSPEC% /C (cmd.exe), not PowerShell — so this PR reverts the ; exit 0 tail (22e3732) and the dispatcher's & call operator (5f46081), both of which were designed for a shell Codex doesn't use.

What stands:

  • Built-ins: the bare portable command (unchanged) plus a cmd.exe commandWindows — 2>nul & exit 0: nul device, & separator (runs exit 0 whatever the dispatch did, restoring the not-found fail-open), matching || true on the POSIX side. Byte-stable: both fields are constant strings.
  • Dispatcher: the plain quoted absolute path "<abs>" hook-dispatch … (no &) — cmd.exe /C and sh both resolve it, and Fix quoted hook commands on Windows openai/codex#33926 wraps the command in an extra quote layer under cmd.

The pair design is what the maintainers originally shipped — only the Windows field's syntax was wrong (PowerShell instead of cmd). Builds without commandWindows support degrade to the bare command, which is portable by itself.

@github-actions

Copy link
Copy Markdown
  • [P1 blocking] Restore startup fail-open — src/builtin-hooks.ts:224. The current bare command again exits nonzero when Codex’s PATH cannot resolve teamai. The dispatcher’s finally cannot run when the CLI never starts, and commandWindows cannot protect POSIX execution. The earlier fail-open finding has regressed.

  • [P2 non-blocking] Seed the Windows wrapper in the test fixture — src/__tests__/codex-team-hooks.test.ts:89. The fixture creates only teamai, while Windows commands now name teamai.cmd, so the wrapper-existence assertion at src/__tests__/codex-team-hooks.test.ts:126 fails on Windows. Create the platform-specific filename or explicitly mock POSIX. The earlier command-prefix assertion issue is resolved; the fixture issue remains.

The PR description includes a representative real-CLI verification record, satisfying the testing requirement. Only the specified diff was read; no PR code was executed, built, or installed.

Per the split agreed in review: the dispatcher entry belongs to Tencent#1210,
so this PR no longer rewrites it — reconcileCodexDispatchers,
getTeamHookDispatchCommand and DISPATCHER_COMMAND are back to main's
state. What remains is the built-in half: the bare portable command
plus the cmd.exe commandWindows (6124e9c).
@kittimzhe

Copy link
Copy Markdown
Contributor Author

Scoped to the built-ins per the split agreed on #1210: the dispatcher hunks are gone (reconcileCodexDispatchers, getTeamHookDispatchCommand, DISPATCHER_COMMAND back to main's state — a9d6994), so this PR now touches only the built-in entries. What stands: the bare portable command plus the cmd.exe commandWindows (2>nul & exit 0). Heads-up @natinli: your test machine still holds the absolute-wrapper dispatcher entries from this branch's earlier iteration — main's DISPATCHER_COMMAND doesn't recognize that form, so wipe the dispatcher entries from ~/.codex/hooks.json before the next inject.

@github-actions

Copy link
Copy Markdown
  • [P1 blocking] Restore startup fail-open — src/builtin-hooks.ts:224. When Codex’s POSIX hook environment cannot resolve teamai from PATH, the new bare command exits 127 instead of succeeding through the previous || true. The dispatcher’s finally never runs because the CLI never starts, and commandWindows cannot protect POSIX execution. This earlier finding remains unresolved.

The earlier Windows fixture finding is resolved for this review: the team-dispatcher implementation and test changes are no longer in the specified diff.

The PR description includes a representative real-CLI verification record, satisfying the testing requirement. Only the specified diff was read; no PR code was executed, built, or installed.

…encent#1215)

Review finding: the bare argv form lost the pre-Tencent#1215 fail-open — under
sh, an unresolvable teamai exits 127 instead of the old || true's 0,
and the dispatcher's finally-exit-0 never runs because the CLI never
starts. Windows keeps the cmd.exe commandWindows from the previous
commit; command runs under sh only, so it can carry the sh tail again:
2>/dev/null || true restores the old not-found behavior exactly (exit
0, message suppressed). Byte-stability is unchanged — the string is a
constant that never depends on the writing machine (Tencent#1074).
@github-actions

Copy link
Copy Markdown
  • [P1 blocking] Keep command runnable when Codex ignores commandWindows — src/builtin-hooks.ts:229. For the Windows Codex configuration described in this PR, the override is ignored and cmd.exe executes teamai … 2>/dev/null || true. On a normal Windows installation, /dev/null does not exist, so redirection prevents TeamAI from starting; the fallback true is also unavailable. Generate a command that works without override support.

The earlier POSIX startup fail-open finding is resolved by restoring 2>/dev/null || true. The Windows fixture finding remains resolved: the team-dispatcher implementation and tests are absent from this diff.

The PR description includes a representative real-CLI verification record, satisfying the testing requirement, though its reported bare commands do not match the current diff.

Only the specified diff was read; no PR code was executed, built, or installed.

@natinli

natinli commented Oct 21, 2026

Copy link
Copy Markdown

Testing the latest bot finding (builtin-hooks.ts:229) on Windows. The conclusion is right; two of its premises are not.

Its premise 1 — "commandWindows is ignored on Windows" — is false

Verified against both binaries:

  • 0.163.0-alpha.10.2 (desktop runtime): command_windows present as a serde variant identifier
  • 0.142.5 (PATH CLI): no such string

discovery.rs:470 (command_windows.unwrap_or(command)) matches the newer binary. Web/desktop Codex reads the override; command does not execute there. @SaulMoro confirmed the same reading from source earlier in #1215.

Its premise 2 — "exits 127" — does not reproduce

Under real cmd:

Command Exit
teamai hook-dispatch SessionStart --tool codex 2>/dev/null || true (teamai on PATH) 0 — 2>/dev/null errors as an invalid path, then || true catches it
same, with teamai removed from PATH 9009 (cmd's not-found code, not 127)

So the POSIX string is noisy but not fatal on the happy path.

The conclusion still holds — for a different reason

If any Windows Codex build does fall through to command (older build, or commandWindows stripped by a project file), that string redirects to the literal path /dev/null, which does not exist, and || true is not a cmd builtin. The [B] row shows the result: 9009, a hard failure before hook-dispatch ever starts.

2>/dev/null || true is not a portable fail-open — it is POSIX syntax that happens to be harmless when the command succeeds.

What I would change

Rather than restoring the POSIX tail on command (which is what this revision does), keep command free of shell syntax entirely:

teamai hook-dispatch <event> --tool codex

and rely on commandWindows for the Windows form. That satisfies:

  • [bug] Codex hook entries on Windows are rendered in bash syntax and silently never run #1074 byte-stability — a constant, no shell tokens, identical on every platform.
  • The reviewer's fail-open concern — on any Windows build that ignores the override, a bare command fails with 9009 rather than dying in a broken redirect. Neither form is fail-open there, but the bare one is at least parseable, and hook-dispatch's own finally { process.exit(0) } covers the case where it actually starts.
  • If genuine fail-open on the command path is required, it has to be expressed in each shell's own syntax, which is exactly why commandWindows exists — not by putting POSIX tokens in a string that may reach cmd.

I have no objection to keeping the tail if the team prefers POSIX-side fail-open; I am flagging that it does not address the Windows case the bot names, and that it makes the string non-portable by construction.

@SaulMoro SaulMoro left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was generated by AI during triage.

Re-review at 67d9098. Here is where the changes requested in #1231 (comment) stand:

Requested State
Use cmd syntax for commandWindows (2>nul & exit 0) Done
Revert the dispatcher changes (#1210 owns them, and it is now merged) Done
Keep command as it is on main Not done. It changed from bash -lc "…" || true to teamai hook-dispatch … 2>/dev/null || true
Make the title and description match the new scope Not done
Add a Windows record: inject leaves the hooks trusted, Codex reports Completed with teamai missing, no stray $null file Not provided

I checked this locally on macOS against the merge of this head into current main (0da2be7). I used the merge because main has changed builtin-hooks.ts since this branch was cut (#1224, #1245).

  • It merges cleanly, npx tsc --noEmit is clean, and the hook suites (builtin-hooks, hooks, hooks-shell-check, codex-team-hooks) pass 85/85.
  • Real CLI, upgrade path: I ran init --agent codex and pull with a main build, then pull again with this branch. Every built-in entry in ~/.codex/hooks.json was rewritten in place, with no duplicates. Each commandWindows became teamai hook-dispatch <e> --tool codex 2>nul & exit 0.
  • A second hooks inject leaves the file byte-identical.
  • With teamai removed from PATH, the new command still exits 0 under both sh -lc and zsh -lc.

Findings

  • [P1 blocking] The title, description and verification record all describe an earlier iteration of this branch: "Codex never reads commandWindows", bare built-ins with no commandWindows, and an absolute-wrapper dispatcher. None of that is in the diff anymore, so the PR has no real-CLI record for its current head. That breaks the Code Review Rules item "The PR description must document sufficient testing … an end-to-end / real-CLI verification record". Please rewrite the title and body around the built-in commandWindows fix and add a record. The change only shows up on Windows, so the record should include a cmd /C run of one generated commandWindows covering three cases:

    • with teamai on PATH, the dispatch runs and exits 0;
    • with teamai off PATH, it still exits 0;
    • no $null file is left in the working directory.

    A run through Codex itself would be better, but as on #1210 a cmd /C run is representative.

  • [P2 non-blocking] src/builtin-hooks.ts:219-231 changes the POSIX command for the Codex tools, and this fix does not need that:

    • Codex only runs command on POSIX, and there main's bash -lc "… 2>/dev/null" || true already works.
    • The comment justifies the change against an earlier state of this branch ("restores the pre-#1215 behavior that the bare form lost"), not against main.
    • It also changes how teamai gets onto PATH. Codex runs $SHELL -lc (codex-rs/hooks/src/engine/command_runner.rs:128-134 at rust-v0.142.5), so without the inner bash -l the user's $SHELL login profile decides PATH instead of bash's.

    I suggest reverting getDispatchCommand and the regex split to main. The fix then comes down to codexWindowsCommand returning `${match[2]} 2>nul & exit 0` plus its comment. If you want to keep the change, explain why in the description.

  • [P2 non-blocking] Some text still says Codex runs Windows hooks in PowerShell:

    • src/builtin-hooks.ts:244-246 (the WINDOWS_PWSH_TOOLS doc)
    • src/hooks.ts:127 (CodexHookEntry.commandWindows)
    • the Unreleased CHANGELOG entry for #1074 (CHANGELOG.md:66), which also states that command is bash -lc "..." || true

    Separately, the codexWindowsCommand doc (src/hooks.ts:1560) says builds without commandWindows support "fall back to the portable bare command". That command is not bare, and under cmd it fails: natinli measured exit 9009 with teamai missing.

  • [P3 nit] src/builtin-hooks.ts:265 adds an empty * line to the getTeamHookDispatchCommand doc, left over from the dispatcher revert.

On natinli's suggestion to make command bare with no tail: that gives up POSIX fail-open, and the Windows fallback it buys also fails (9009). Current Codex reads commandWindows, so keeping main's POSIX command is the smaller change.

Verdict: Request changes.

kittimzhe and others added 2 commits October 22, 2026 09:39
…ent#1215)

Per the Windows measurements in the thread: the bash-wrapped command
kept its POSIX fail-open all along, and the only real defect was the
commandWindows syntax — PowerShell where Codex runs %COMSPEC% /C
(cmd.exe). Everything else this PR carried (bare argv, sh tail,
dispatcher rework) iterated on a wrong premise and is reverted to
main; the diff is now the one substantive fix plus its contracts:

- codexWindowsCommand emits the cmd form: nul redirect, & separator,
  exit 0 — parseable by the real shell, restoring the not-found
  fail-open there (2>$null created a file named $null; ';' is an
  argument delimiter in cmd, so 'exit 0' arrived as junk arguments).
- Reconcile upgrades entries older teamai wrote with the PowerShell
  form, so existing installs heal on the next inject (fixtures moved
  to the new baseline).
@kittimzhe

Copy link
Copy Markdown
Contributor Author

Shrank to the one substantive fix per natinli's Windows measurements: the bash-wrapped command had its POSIX fail-open all along, and the only real defect was the commandWindows syntax — PowerShell where Codex runs %COMSPEC% /C. The bare-argv and sh-tail revisions iterated on a wrong premise and are reverted to main; what remains:

  • codexWindowsCommand emits the cmd form — 2>nul & exit 0 (nul device, & separator): parseable by the real shell, restoring the not-found fail-open where it actually runs.
  • Reconcile upgrades entries older teamai wrote with the PowerShell form, so existing Windows installs heal on the next inject (fixtures moved to the new baseline, snapshot updated).

The bot's fail-open concern is resolved by construction — command never lost it. Diff is now 4 files: the syntax fix, its contract test, and the fixture/snapshot baseline.

@github-actions

Copy link
Copy Markdown
  • [P2 non-blocking] Align the PR description with the current implementation — src/hooks.ts:1566. The diff retains bash-wrapped command entries and changes commandWindows to cmd.exe syntax; the description and verification record instead claim bare commands, no commandWindows, and absolute team-dispatcher wrappers. Update the description to reflect what this PR actually changes.

Earlier startup fail-open and Windows test-fixture findings are resolved for this diff: the offending built-in-command and team-dispatcher changes are absent. The last Windows fallback finding also concerns a command change no longer present; I am not re-raising unchanged base behavior.

The description includes a representative real-CLI record, satisfying the testing-record requirement, although it describes a different implementation. Only the specified diff was read; no PR code was executed, built, or installed.

@SaulMoro SaulMoro left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was generated by AI during triage.

@kittimzhe This re-reviews f1ad8d29 against our Request changes at 67d90981 (#1231 (review)). The code change is right. What's pending is the PR text and its record, so the Request changes stays until those are updated.

We checked the cmd premise against Codex's source. On Windows, codex-rs/hooks/src/engine/discovery.rs uses commandWindows when it is set, and command_runner.rs runs it through %COMSPEC%, which falls back to cmd.exe. So 2>nul & exit 0 is the right form.

We built this head and main (d60e68f5) and ran teamai hooks inject in an isolated HOME. main writes 7 Codex commandWindows entries as ... 2>$null; exit 0. This head rewrites all 7 in place to ... 2>nul & exit 0, and nothing else in ~/.codex/hooks.json changes. A second inject is byte-identical, and every inject exited 0.

Status of our earlier requests

  • Keep command as on main: done.
  • [P1 blocking] Title and description: still open. They describe the reverted implementation: bare argv built-ins, an absolute team-dispatcher path, and no commandWindows. The real-CLI record in the body is for that version, so this diff has no record. Code Review Rules: a runtime change needs an end-to-end / real-CLI record. Updating the title and summary, and pasting one hooks inject run showing the rewritten commandWindows, closes it. No code change is needed.
  • [P3 nit] Windows record: either say Windows was not run, or paste natinli's measurement of the cmd form.

Other findings

[P2 non-blocking] Two comments still say Codex runs hooks with PowerShell on Windows: the commandWindows field doc at src/hooks.ts:127, and src/builtin-hooks.ts:231-235, which says toCodexEntry fills it with "the PowerShell form".

[P3 nit] CHANGELOG.md:70 has the same PowerShell wording.

#1263 also edits src/hooks.ts, but no lines overlap. This PR owns the built-in commandWindows string, and #1263 owns the trust fingerprint and where member hook groups go. #1263's trust key still includes the commandWindows strings, so this rewrite still triggers a new trust pass.

@natinli

natinli commented Oct 22, 2026

Copy link
Copy Markdown

Windows record for the commandWindows string at f1ad8d29, so the P3 nit does not need to be written from scratch.

Environment

OS Windows 11 Pro 10.0.26200
Codex 0.142.5 on PATH; 0.163.0-alpha.10.2 desktop
Shell Codex uses for hooks %COMSPEC% /C → cmd.exe

String under test

teamai hook-dispatch SessionStart --tool codex 2>nul & exit 0

Run from a real cmd /C (batch file, so nothing is lost to Git Bash argument handling), in a scratch directory.

Results — new (cmd) form

Case Exit Stray $null file
teamai on PATH 0 no
teamai removed from PATH (C:\Windows\System32 only) 0 no

Control — the old (PowerShell) form, same two cases

teamai hook-dispatch SessionStart --tool codex 2>$null; exit 0
Case Exit Stray $null file
teamai on PATH 0 YES — a zero-byte file literally named $null
teamai removed from PATH 1 YES

Both defects the change is meant to fix are visible side by side: the old form leaves a stray $null file in the working directory in both cases, and loses fail-open (exit 1) when teamai is missing. The new form produces neither.

Note $null here is a real filename on disk (if exist "$null" is true), not the PowerShell automatic variable — that is the failure mode being corrected.

Caveat

I ran cmd /C directly rather than through Codex, consistent with your note that an unquoted command makes the two equivalent. The hooks inject rewrite itself is covered by your isolated-HOME run.

Scratch files deleted; the real ~/.codex was not touched.

@SaulMoro SaulMoro left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was generated by AI during triage.

Approving f1ad8d29. The change is correct: Codex on Windows runs commandWindows through %COMSPEC% (cmd.exe), and teamai hooks inject rewrites the existing PowerShell entries in place to 2>nul & exit 0. CI is green. The PR title and description still describe the reverted approach, so the squash commit gets a subject that matches the diff.

@SaulMoro
SaulMoro merged commit 611f467 into Tencent:main Oct 22, 2026
17 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.

Codex: built-in commandWindows uses PowerShell syntax, but Codex runs Windows hooks under cmd.exe

4 participants