Harden codex-review.sh: review in an isolated temp worktree (Closes #9) - #11
Conversation
Run `codex exec review` inside a detached, throwaway git worktree checked out at the PR head instead of `gh pr checkout <PR#> --force` against the operator's own checkout. This eliminates the force-reset class entirely: the operator's branch, index, working tree, and unpushed commits are never touched, so the read-only reviewer can no longer mutate operator state. - Fetch the PR head fork-safely (`git fetch origin pull/<PR#>/head <base>`) so fork PRs work too; resolve FETCH_HEAD and add the worktree there. - Review against the qualified, freshly-fetched `origin/<base>`. - `trap ... EXIT` removes the temp worktree (`git worktree remove --force`) and temp file even on failure; `git worktree prune` tolerates a stale worktree from a hard-killed run, so re-runs are safe. - Drop the now-unneeded clean-worktree guard; the reviewer works even with local uncommitted changes. - Preserve all invariants: `-c sandbox_mode="read-only"`, comments-only, verbatim `gh pr comment` with the cross-vendor header, `unset GH_REPO` + cwd-derived `--repo`, `-m <model>` passthrough, current-repo only. - Update the script header + reviewer/codex-review.md to describe the temp-worktree approach; drop force-checkout / clean-guard references. Closes #9 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Codex reviewer (cross-vendor, read-only)Posted verbatim by The patch mostly improves isolation, but the unconditional repo-wide worktree prune can affect unrelated operator worktrees and violates the stated isolation guarantee. Review comment:
|
Codex reviewer (cross-vendor, read-only)Posted verbatim by The new isolated worktree flow can review against a stale base because the rewritten fetch no longer refreshes the remote-tracking base ref used by Codex. Review comment:
|
…ktree prune Address Codex review on PR #11 (issue #9): P1 — Stale/missing remote-tracking base ref. The previous source-only fetch (`git fetch origin pull/<PR#>/head <base>`) only updated FETCH_HEAD, leaving `refs/remotes/origin/<base>` stale or missing, so a re-run after the base advanced could review against an old base (or fail in a clone lacking that tracking ref). Now fetch with explicit destination refspecs: `git fetch --force origin pull/<PR#>/head:<tmpref> <base>:refs/remotes/origin/<base>`, so `--base origin/<base>` always diffs against a current base. The head goes to a private local ref (resolved, then deleted) to avoid FETCH_HEAD ambiguity. P2 — Repo-wide `git worktree prune` mutated operator state. Removed the global prune entirely (it could drop registry metadata for unrelated operator worktrees past gc.worktreePruneExpire). Cleanup now removes only this script's own temp worktree via the trap; re-run safety comes from each run using a fresh mktemp path. Updated the script header block and reviewer/codex-review.md to match (refspec fetch; no global prune). All invariants preserved: isolated detached temp worktree, no gh pr checkout, no bare --force on the operator's branch, -c sandbox_mode="read-only", comments-only verbatim gh pr comment with the cross-vendor header, unset GH_REPO + cwd-derived --repo, -m <model> passthrough. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Coder revision — round-1 (addresses both Codex findings)Both findings fixed in [P1] Stale/missing remote-tracking base ref. The fetch was source-only so [P2] Repo-wide Docs in sync: updated the script's header comment block and Invariants preserved: isolated detached temp worktree; no Verified statically (did not run the harness against a live PR, to avoid posting a |
Codex reviewer (cross-vendor, read-only)Posted verbatim by The new isolated worktree flow is generally sound, but the explicit fetch uses an unqualified base ref that can break valid repositories with same-named branch/tag refs. That can prevent the reviewer from running on affected PRs. Review comment:
|
The base-ref fetch used unqualified refspec sources. If a base branch name also exists as a tag on origin (e.g. a release branch and tag both named v1.2.0), the unqualified source can resolve ambiguously or fail before Codex runs. Qualify both sources: pull/<PR#>/head -> refs/pull/<PR#>/head <base> -> refs/heads/<base> Fetch behavior is otherwise unchanged. Header comment block and reviewer/codex-review.md updated to quote the qualified refspecs. Addresses round-2 Codex [P2] finding on PR #11. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Coder revision — round-2 (addresses Codex [P2])Fixed in [P2] Unqualified fetch refspec sources. The base-ref Fetch behavior is otherwise unchanged — same Docs in sync: updated the script's header comment block (both the top isolation Invariants preserved: isolated detached temp worktree; no Verified statically (did not run the harness against a live PR): |
Codex reviewer (cross-vendor, read-only)Posted verbatim by The patch can still mutate operator Git state by force-updating local tags during the fetch, which conflicts with the read-only invariant the change is meant to strengthen. Review comment:
|
Faber — escalating to human (round-3 cap)CI has been green every round. Reached the ~3-round cap without convergence, so flagging Round history (all findings valid, all accepted by the coder):
Why escalating, not auto-fixing: each round resolves the prior finding but the reviewer surfaces a progressively more marginal operator-state edge case in the same Decision left to the human (who holds the merge gate). |
The PR head + base fetch used a global `git fetch --force`. Git auto-follows tags, so a global `--force` could also force-update local `refs/tags/*` if origin moved a tag reachable from the fetched commits — an operator-state mutation that violates the script's read-only guarantee. Stop forcing globally: force-update ONLY the two destination refs we own via `+`-prefixed refspecs, and pass `--no-tags` to disable tag auto-following. The fetch now touches nothing outside the PR-head temp ref and origin/<base>. Header comment block and reviewer/codex-review.md step 1 updated to match. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Coder revision — fetch no longer mutates operator tags (
|
Codex reviewer (cross-vendor, read-only)Posted verbatim by The temp-worktree approach is mostly sound, but the shared temporary ref creates a concrete race for concurrent invocations in the same repository and can lead to reviewing/commenting on the wrong PR head. Review comment:
|
Faber — PR #11 capped at final round (recommend MERGE)Final fix ( Codex's final re-review raised one further [P2]: the shared Recommendation: merge as-is. The substantive issue-#9 hardening is complete and CI is green:
The findings Codex peeled off across rounds 1–final are progressively more theoretical edge cases (name-colliding branch/tag, moved origin tags, concurrent same-repo runs) — none of which exist or occur in this control-plane repo's sequential loop. Optional future follow-up if you ever want the script safe for concurrent manual use: a per-run unique PR-head ref (PID/temp suffix). Not needed for the loop. Say the word and I'll open an issue. |
What & why
scripts/codex-review.shpreviously checked the PR out into the operator's own checkout viagh pr checkout <PR#> --force. Even behind the clean-worktree guard,--forcecould silently discard unpushed local commits — a reviewer documented as read-only / comments-only must never be able to mutate operator state.This rewrites the checkout/review section to run the review in an isolated, detached, throwaway git worktree at the PR head. The operator's branch, index, working tree, and unpushed commits are provably untouched, so the entire force-reset / clean-guard class is eliminated and "read-only" is literally true.
Closes #9
Changes
git fetch origin pull/<PR#>/head <base>brings the head commit into the object store even for fork PRs (a plaingit fetch originwould not) and refreshesorigin/<base>. ResolveFETCH_HEADandgit worktree add --detach <tmpdir> <head>.codex exec -C <tmpdir> review -c sandbox_mode="read-only" --base origin/<base> -o <tmp>. (-Cis a parent-codex execflag, so it precedes thereviewsubcommand — verified againstcodex-cli 0.129.0.)trap ... EXITrunsgit worktree remove --force <tmpdir>(rm -rf fallback) +rm -f <tmp>;git worktree prunefirst tolerates a stale worktree from a hard-killed run. Re-run safe.-c sandbox_mode="read-only"(never--dangerously-bypass-*); comments-only; verbatimgh pr commentwith the cross-vendor header;unset GH_REPO+ explicit cwd-derived--repo;-m <model>passthrough (no hardcoded model); current repo only.reviewer/codex-review.mdto the temp-worktree mechanism; dropped force-checkout / clean-guard references.RESTORE.mdandREADME.mdneed no change (confirmed no dangling references).How tested
shellcheck scripts/codex-review.sh— clean; samefind . -name '*.sh' | xargs shellcheckas CI — clean.gh pr checkout, no bare--force(onlygit worktree remove --force).codex exec -C <dir> reviewparses;codex exec review -Cdoes not.