fix(agent-setup): stop agent wrappers from resolving themselves as the real binary
What & why
Fixes #4987.
find_real_binary in the agent wrappers decides "this candidate is not me" by directory name only: the absolute getBinDir() captured when the wrapper is generated, plus $HOME-relative patterns. It never checks whether a candidate is the wrapper. Any PATH entry that reaches the wrapper under another name gets accepted as the real binary, so the wrapper runs itself and starts over.
#4987 reports one way in (a symlink to the wrapper earlier on PATH). There are more:
- Different or fixture
HOME, with the bin dir reached through an alias (~/.supersetis a symlink to~/.paradev). The HOME-relative skip no longer matches, so/Users/<me>/.superset/bin/codexis "the real codex". A test harness that set a fixtureHOMEand kept the realPATHhit this on macOS. It piled up about 1,900 wrapper processes and pushed load to 108 on 36 cores before it was killed by hand. - Relative or duplicate PATH entries (
dir//,dir/./,./rel-bin).${dir%/}strips only one trailing slash. - Wrappers from two app homes on PATH (
~/.paradev-*). Each skips only its own dir, so they can pick each other.
The codex wrapper is the worst case. It runs the real binary as a child (the others exec), appends --enable hooks on every pass, and its session watcher is not first-wins like the launch report. Each pass leaves more processes behind. A timeout on the parent does not stop it: killing the top process orphans the rest of the chain, and it keeps forking.
Changes, all in packages/agent-setup/src/agent-wrappers-common.ts:
find_real_binaryskips a candidate that is the same file as the running wrapper ([ "$candidate" -ef "$0" ], which follows symlinks), or whose second line starts with# Superset agent-wrapper(any other wrapper, any version). Neither check depends on HOME. The existing directory skip list stays.- Loop guard in every wrapper.
SUPERSET_WRAPPER_DEPTH_<binary>goes up by one per pass and is inherited by both child andexecre-entry. At 8 the wrapper printsParadev: <binary> wrapper re-entered itself 8 times; a PATH entry resolves <binary> back to this wrapper (<path>). Remove it from PATH.and exits 126. The value is read as decimal digits only (junk counts as 0,09is 9). - bash 3.2 only (
read -n,-ef,10#,case). Checked against macOS/bin/bash3.2.57. No extra processes per PATH entry.
Follow-ups I didn't take on here:
- Make the codex session watcher first-wins, like the launch report.
- Add the remaining wrappers (amp, droid, gemini, grok, kimi, vibe, devin, muse, mastracode, agy) to the parametrised test. They share the generator.
shell-wrappers.tsPATH handling was only skimmed, not audited for the same problem.
How I tested it
New test: packages/agent-setup/src/agent-wrappers-self-recursion.test.ts, 11 cases for each of 6 wrappers (claude, codex, opencode, copilot, ufo, cursor-agent). None of the tests can loop. Resolution cases run only the extracted find_real_binary function, which prints a path. Whole-wrapper cases use PATH=<stub dir>:/usr/bin:/bin, where the stub only echoes, and preset the depth counter to check the guard.
- Old generator (with the new constants added so the test file imports): 6 pass, 60 fail. The failures show the resolver returning the wrapper's own alias path where the stub was expected. The 6 passes are the "still finds a plain real binary" cases.
- This branch, head
32a0bb2b65629f0efe1fab788e0c91358d4af5bc: 66 pass, 0 fail for the new file.bun testinpackages/agent-setup: 535 pass, 1 skip, 0 fail. bun run typecheck(agent-setup) andbun run lint: clean.psafter each run: no--enable hooks --enable hooksprocesses left.
Not run on purpose: a real recursion against the fix. An earlier draft of this test did run the loop under spawnSync with a timeout and SIGKILL, and it got away the same way (orphaned chain, thousands of processes). So the guard is proven by presetting its counter, and the resolver by calling it directly.
Checklist
- PR title follows conventional commits (
type(scope): subject) -
bun run lintandbun run typecheckpass (CI fails on lint warnings too) - "Allow edits from maintainers" is checked on fork PRs
🤖 Generated with Claude Code
Summary by cubic
Fixes agent wrappers resolving themselves as the real binary, which caused unbounded process loops (Fixes #4987). find_real_binary now skips the wrapper itself via -ef (catching symlinks, aliased bin dirs, and relative PATH entries) and any other Superset wrapper via its marker line, so the check no longer depends on HOME.
- Each wrapper counts passes in
SUPERSET_WRAPPER_DEPTH_<binary>and exits 126 with a clear message after 8 re-entries, as a last-line guard if resolution ever regresses. - The depth counter is inherited across child and
execre-entry, parsed as decimal, and per-binary. - Added 66 test cases covering aliased homes, symlinks, relative PATH entries, wrappers from multiple app homes, and the guard's limit and counter behavior.
Summary by CodeRabbit
- Bug Fixes
- Agent command wrappers now avoid selecting another wrapper as the underlying executable, including when wrapper paths are aliased or symlinked.
- Excessive wrapper nesting now stops with a clear diagnostic instead of continuing indefinitely. Invalid nesting counts are handled safely, and nesting limits are tracked separately for each command.