Repository navigation
fix(init): return exit code 2 when codex uninstall guard refuses hook (fixes #4164) - #4463
Conversation
📊 Automated PR Analysis
SummaryChanges uninstall_codex to return a typed error so that when the symlink-containment guard refuses to touch a hook file outside the project, rtk exits with code 2 instead of 0, letting scripts detect partial cleanup. However, the actual diff bundles in a large amount of unrelated content: a new OpenCode plugin and AGENTS.md with aggressive 'always use this tool, never grep/read' directives, two ad-hoc GitHub-querying scripts, and unrelated curl/docs changes. Review Checklist
Linked issues: #4164 Analyzed automatically by wshm · This is an automated analysis, not a human review. |
KuSh
left a comment
There was a problem hiding this comment.
Round 1 of 3 — CHANGES REQUESTED
Claim: rtk init --codex --uninstall exits 2 when the containment guard refuses the hook path, so a script can tell a partial uninstall from a full one (#4164).
Scope: accept if narrowed to "the #4164 change only — src/hooks/init/codex.rs, src/hooks/init/mod.rs and the exit-code mapping, rebuilt on current develop" (round 1, frozen). The branch carries 13 other files: the read.rs and curl commits from #4367/#4370 (both closed), the OpenCode claude.rs/docs changes, and local agent tooling (AGENTS.md, .opencode/, .gitattributes/.gitignore graphify entries, check_prs.py, find_candidates.py). The tooling cannot land in any form: AGENTS.md is read by every agent working in this repo. The read.rs part does not compile (src/cmds/system/read.rs:367, a doubled mod tests {), and the branch conflicts with develop, where main.rs's routing moved to src/lib.rs. It also overlaps #4269 in codex.rs/mod.rs; once narrowed, this one lands first.
Ran: since the head does not build, b6b250a's codex.rs/mod.rs/main.rs hunks were ported onto develop (a pub use codex::UninstallCodexError was needed, because codex is a private module and the import fails with E0603). Merge-base binary vs that port, × 7 cases: the #4164 repro (.codex symlinked outside), the same with --dry-run, .codex/hooks.json.bak symlinked outside, a clean install→uninstall, nothing installed, an invalid hooks.json, and a global install→uninstall with ~/.codex symlinked elsewhere. Exit codes and both streams were compared. Invariants checked: split streams, every decision. Under the hook, rtk rewrite leaves rtk init alone. Savings are not applicable.
Pins: 4 decisions, 4 mutated, 3 survived the suite (mod.rs HookRefused arm, the exit-2 mapping, From<anyhow::Error> → Other).
Matrix: never run before this round; running now. Pin check: never run before; running now.
CI @b6b250a7: no CI run exists. The branch conflicts, so the CI workflow never started, and only the CLA check is present. Locally the head fails to compile.
Blocks merge (3, frozen at round 1)
-
src/hooks/init/mod.rs:865— exit 2 is unreachable: the error loses its type one hop beforerun_cli.HookRefused(e) => return Err(e)returns the inneranyhow::Error, sodowncast_ref::<UninstallCodexError>()inrun_clinever matches. Evidence, before → after:- #4164 repro: exit 0 → exit 1. The guard message prints twice, once in the stdout report and again as
rtk: …on stderr. --dry-run: exit 0 → exit 1, and the[dry-run] Nothing written.footer is gone, because the early return skipsprint_dry_run_footer()..baksymlink: exit 0 → exit 1.
A correct fix gives these rows:
Case Exit Output .codexsymlinked outside2 stderr does not repeat the report hooks.json.baksymlinked outside2 same refused case with --dry-run2 [dry-run] Nothing written.still printedclean uninstall 0 nothing installed 0 invalid .codex/hooks.json1 - #4164 repro: exit 0 → exit 1. The guard message prints twice, once in the stdout report and again as
-
src/hooks/init/codex.rs:517— the guard trips at two sites, and only one test was updated. This return also fires whenhooks.json.bakresolves outside the project (ensure_inside_project(&backup_path_for(..))).test_uninstall_refuses_a_backup_sibling_that_leaves_the_project(:2069) still.expectsOk, so it fails: 331 passed, 1 failed on the ported change. It needs the sameexpect_err+HookRefusedassertion as the.codex-symlink test. -
src/main.rs:2578— unpinned behaviour (the exit-code mapping). These mutations pass everyhooks::initandinit_uninstalltest:- mod.rs
HookRefused(_) => {} return Ok(2)→return Err(e)From<anyhow::Error>producingHookRefusedinstead ofOther
The one updated test checks what
uninstall_codexreturns, not the process exit. A test that runsrtk init --codex --uninstallthroughcommon::rtk_command()in a temp project must assert the six rows in item 1, and each of those three mutations must turn it red. - mod.rs
Optional — will not hold merge
HookRefused'sDisplayprints the inner error, andsource()returns that same error. Any path that reports it throughentry()instead of mapping it to 2 prints the guard text twice (rtk: hook refused: X: X). Dropping{e}fromDisplay, or returningNonefromsource(), avoids that.- The enum carries one bit through three layers and a downcast. An alternative is for
uninstall_codexto return an outcome (Ok(hook_refused: bool)or an exit code) thatrun_climaps to 2, with no downcast. Either shape is fine; the rows above are what matters.
Filed as follow-ups
- #4589 Codex uninstall report says "not installed (nothing to remove)" and then "the hook may still be registered" — pre-existing, outside this PR's hunks; filed.
Checked and correct — no need to re-verify
uninstall_codexreturnsHookRefusedexactly whenhook_left_in_placeis set, after the report is printed and afterAGENTS.md/RTK.mdare cleaned. The rest of the uninstall still happens in both refused cases. This is pinned bytest_uninstall_leaves_a_hooks_file_that_sits_outside_the_project_alone.--globalnever setshook_left_in_place, so a global uninstall keeps exit 0.- A clean install→uninstall round-trip exits 0 and removes the hook,
RTK.mdand theAGENTS.mdreference.
Your questions
- (none; thanks for flagging that the branch was generated and unreviewed — this round is meant to give you the exact list.)
Next
Rebuild the branch on develop with only the #4164 change: codex.rs, hooks/init/mod.rs, and the mapping in src/lib.rs, with UninstallCodexError re-exported from hooks::init. Then fix 1, update the test in 2, add the exit-code test in 3, and run cargo fmt (the mapping line and import order fail it).
Rounds: 1/3. Threads: 3 open (waiting on the author), 0 resolved this round.
b6b250a to
779ff92
Compare
|
Addressed the review feedback from @brian-rtk:
|
An uninstall that fails for another reason exits 1, and every other agent's completed uninstall exits 0. The codex test now also checks that AGENTS.md is cleaned when the hook is refused. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
KuSh
left a comment
There was a problem hiding this comment.
Round 2 — APPROVED
Claim: rtk init --codex --uninstall exits 2 when the containment guard refuses the hook path, so a script can tell a partial uninstall from a full one (#4164).
Scope: accept if narrowed to "the #4164 change only, rebuilt on current develop" (round 1, frozen). The narrowing has been met: the branch is one commit touching codex.rs, hooks/init/mod.rs, src/lib.rs and a test. It merges cleanly onto develop 7e92da0.
Ran: merge-base 5ab6291, head 779ff92, and the head merged onto develop. Each ran 6 codex uninstall cases:
.codexsymlinked outside;- the same with
--dry-run; hooks.json.baksymlinked outside;- a clean install→uninstall;
- nothing installed;
- invalid
.codex/hooks.json.
They also ran 16 install→uninstall rows across every other agent (Claude, Gemini, Cursor, Pi, OMP, Trae, Antigravity, Droid, Vibe, Hermes, Copilot, the local and -g forms, the error paths). Invariants: split streams, every decision. Savings not applicable.
Pins: 16 decisions, 16 mutated. 11 survived the PR's suite: the error-path exit 1, and the 10 Ok(0) exit codes now returned by the other agents' uninstalls. All 11 are killed by the fixup below. So are 3 more aimed at the new assertions: a refused dry run that writes, and two refusals that are reported but not honoured.
Matrix: executed round 1, rerun this round on the new head. Pin check: executed round 1, rerun this round on the new head.
CI @779ff920: all green (15/15). @cdbc41001: all green (15/15).
Blocks merge (3, frozen at round 1) — all closed
-
Fixed: exit 2 is now reached. Before → after on the head:
Case Before After .codexsymlinked outside0 2 hooks.json.baksymlinked outside0 2 refused --dry-run0 2, footer kept On the head, clean and no-op uninstalls exit 0, invalid JSON exits 1, and stderr is empty in every refused case.
AGENTS.mdis still cleaned when the hook is refused. -
Fixed: both guard tests (
.codexsymlink and.baksibling) assertHookRefused. -
Fixed:
test_codex_uninstall_exit_codeskills all 6 mutations of the codex decisions:- the refused-or-clean choice;
- both arms of the exit-code match (each mapped to the other's code);
- the dry-run footer order;
- dropping the code in
run_cli.
Two gaps remained, and the fixup commit closes both:
- Error path: the invalid-
hooks.json→ 1 row was not asserted, sounwrap_or(HookRefused)on the uninstall result passed the suite. It is now case 4 oftest_codex_uninstall_exit_codes. - Other agents:
uninstall_with_patch_modeanduninstall_init_dispatchnow return a code, which is the shape my round-1 note suggested. That gave 10 other agents' uninstalls anOk(0)no test checked: changing any of them toOk(2)passed the suite. The newtests/init_uninstall_exit_code_test.rsinstalls and uninstalls 13 agent forms: the 10, project-scoped Pi and OMP, and global Codex. Each must report "RTK uninstalled" and exit 0. Each of the 10 mutations turns it red, and so does making the global Codex uninstall report a refusal.
The fixup also tightens
test_codex_uninstall_exit_codes. Each of these checks fails if its behaviour regresses:- The refused cases now start from a registered RTK hook, so an unhonoured refusal has something to rewrite.
- The "Project cleanup still performed" assertion held whether or not cleanup ran. It now checks that the user's
RTK.mdis kept, thatAGENTS.mdloses its@RTK.mdline, and that the hook outside the project is left alone. Reporting the refusal but still editing the hook turns it red. - The refused
--dry-runstarts from a fresh@RTK.mdand must leave it in place. Passingdry_run: falseintouninstall_codexturns it red. - The
.bakcase checks that stdout explains the refusal, that the project's hook stays registered, and that nothing is written through the symlink. Refusing only on the.bakcheck while still writing turns it red. - The real install→uninstall must report "RTK uninstalled for Codex CLI" and leave no
rtk hook codexentry. - The invalid-JSON case checks that stderr names
.codex/hooks.jsonand that the file is left untouched.
Filed as follow-ups
- #4589 Codex uninstall report says "not installed (nothing to remove)" and then "the hook may still be registered". Filed at round 1; outside this PR's hunks.
- #4156
rtk init --codexwrites through a symlinkedAGENTS.md. The uninstall does the same, ondevelopand on this head. Outside this PR's hunks; comment added with the repro. - #3404 Kilocode, kimi, windsurf and cline uninstalls remove nothing of the agent; kilocode and kimi exit 1 with "Uninstall only works with --global flag". Found while writing the fixup's agent list, outside this PR's hunks, and already tracked by #3404.
Checked and correct — no need to re-verify
- On the 16 other-agent rows, exit code, stdout and stderr are identical to the merge-base (temp paths normalised).
return uninstall_init_dispatch(...)skips nothing:run_clireturnsOk(code)right after the match.- A global Codex uninstall cannot hit the guard and still exits 0.
- The fixup merged onto
developpassescargo fmt --checkandclippy --all-targetswith 0 warnings.cargo test --allpasses, except one test that also fails ondevelopon this machine (go_test_help_is_not_replayed_through_the_injected_json, a local toolchain shim).
Your questions
- (none)
Next
Fixup cdbc410 pushed (test-only, code-reviewed): it pins exit 1 on an ordinary uninstall failure and exit 0 with a reported removal for every other agent's uninstall, and makes the codex cases check what each claims. Approving. Thanks for the clean rebuild.
Threads: 0 open, 3 resolved this round.
Fixes #4164.
When
rtk init --codex --uninstallruns in a project where.codexis a symlink pointing outside the project, the containment guard refuses to touch the hook file. Previously the command printed a clear warning but exited 0, making it impossible for scripts to detect partial cleanup.Changes:
UninstallCodexErrorenum incodex.rswithHookRefusedandOthervariantsuninstall_codexnow returnsResult<(), UninstallCodexError>and returnsHookRefusedwhen the guard triggersuninstall_with_patch_modepropagatesHookRefusedas-isrun_cliinmain.rsmapsHookRefusedto exit code 2test_uninstall_leaves_a_hooks_file_that_sits_outside_the_project_aloneto expect the errorExit codes:
@KuSh — ready for review.