fix(cli): honor ZDOTDIR/XDG_CONFIG_HOME in completion install (#63069)#81298
fix(cli): honor ZDOTDIR/XDG_CONFIG_HOME in completion install (#63069)#81298MoerAI wants to merge 3 commits into
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 5c935bf8af
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| } | ||
| } | ||
| return path.join(home, ".config", "powershell", "Microsoft.PowerShell_profile.ps1"); | ||
| return candidates[0]!; |
There was a problem hiding this comment.
Preserve bash_profile fallback when no profile exists
When no bash profile file exists yet, getShellProfilePath() now returns candidates[0] unconditionally, which resolves to ~/.bashrc; this changes the previous fallback behavior that installed into ~/.bash_profile when ~/.bashrc was absent. In environments that launch bash as a login shell (common on macOS), .bash_profile is the startup file read by default, so creating only .bashrc can leave completion inactive immediately after install even though installation reported success.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Thanks for the review. Re-checking against pre-PR main, the concern doesn't reproduce — the new code preserves the existing fallback behavior, it doesn't change it.
Pre-PR (src/cli/completion-runtime.ts on main before this branch):
if (shell === "bash") {
return path.join(home, ".bashrc");
}Old code returned ~/.bashrc unconditionally — it never wrote to .bash_profile, regardless of which file existed.
This PR (src/cli/completion-runtime.ts after the change):
if (shell === "bash") {
return [path.join(home, ".bashrc"), path.join(home, ".bash_profile")];
}plus getShellProfilePath() returns the first existing candidate, or candidates[0] (= .bashrc) when none exists.
So in the specific "no profile file exists" case the PR singles out:
- Old behavior: install to
~/.bashrc - New behavior: install to
~/.bashrc(same)
The strict improvement over old code is that when .bash_profile does exist and .bashrc does not (the macOS login-shell case), the new resolver picks .bash_profile instead of writing to .bashrc and silently disagreeing with the user's actual shell startup file. That's the documented intent of #63069 and is exercised by the new regression returns true when bash completion is installed in .bash_profile only (macOS).
If you'd like the install target to prefer .bash_profile on macOS (Darwin) when neither file exists, that's a strictly new behavior change beyond the linked issue; happy to file a follow-up. For this PR I'd like to keep the .bashrc-first ordering since the prior shipped behavior was already .bashrc-only.
CC: the same regression also covers $ZDOTDIR/.zshrc and $XDG_CONFIG_HOME/fish/config.fish priority over $HOME (isCompletionInstalled reads from the same resolver as installCompletion, so check/install can't drift).
|
Codex review: needs real behavior proof before merge. Reviewed June 24, 2026, 10:18 AM ET / 14:18 UTC. Summary PR surface: Source +22, Tests +236. Total +258 across 5 files. Reproducibility: yes. from source inspection. Current main still ignores ZDOTDIR/XDG_CONFIG_HOME for the central bug, and the PR-introduced bash hint/default issues are traceable through Review metrics: 1 noteworthy metric.
Root-cause cluster Members:
Proposal only: this assessment does not dispatch repair, suppress jobs, mutate sibling items, close, or merge anything. Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Proof guidance:
Risk before merge
Maintainer options:
Next step before merge
Security Review findings
Review detailsBest possible solution: Land a refreshed branch that centralizes shell-profile candidate resolution while using one selected existing-aware profile path for install, status, and user-facing hints, preserving the shipped bash fallback unless maintainers explicitly approve a default change. Do we have a high-confidence way to reproduce the issue? Yes from source inspection. Current main still ignores ZDOTDIR/XDG_CONFIG_HOME for the central bug, and the PR-introduced bash hint/default issues are traceable through Is this the best way to solve the issue? No. Candidate-based resolver centralization is the right shape, but the current patch is not the best fix until bash install and reload hints share the same selected profile path and the bash fallback change is either reverted or explicitly approved. Full review comments:
Overall correctness: patch is incorrect AGENTS.md: found and applied where relevant. Codex review notes: model internal, reasoning high; reviewed against acc2a0ee7297. Label changesLabel justifications:
Evidence reviewedPR surface: Source +22, Tests +236. Total +258 across 5 files. View PR surface stats
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
5c935bf to
ca67a8b
Compare
|
Rebased on latest |
|
CI status note (head
Confirming pre-existing on This PR only touches |
|
Rebase status (Day 5): Upstream has landed several related changes to
Current To land cleanly I'll need to:
Issue #63069 is still OPEN and |
…aw#63069) Shell completion install/check helpers resolved zsh, fish, and bash profile paths through a single canonical-path resolver. On macOS with $ZDOTDIR=$HOME/.config/zsh the reporter saw completion silently written to $HOME/.zshrc - a file the shell never reads when $ZDOTDIR is set, so completion never activated in new shells. The same gap applied to fish + $XDG_CONFIG_HOME, and bash check/install disagreed about .bashrc vs .bash_profile on macOS-only installs. This change extends the upstream API around resolveCompletionProfilePath rather than replacing it: - Adds exported resolveCompletionProfilePathCandidates(shell, options) returning candidate paths in priority order. zsh honors $ZDOTDIR exclusively when set (no $HOME fallback that the shell would not read); fish honors $XDG_CONFIG_HOME exclusively when set; bash returns [.bashrc, .bash_profile]; PowerShell preserves the exact upstream win32 SHELL/USERPROFILE logic. - resolveCompletionProfilePath(shell, options) remains synchronous and returns the canonical install target (first candidate). All upstream callsites keep the same signature. - New private resolveExistingCompletionProfilePath(shell, options) picks the first existing candidate so check + install always agree on the same file. Falls back to the canonical target when nothing exists yet, so first-time installs still resolve to a stable path. - isCompletionInstalled, usesSlowDynamicCompletion, and installCompletion route through the new existence-aware resolver. installCompletion drops the per-shell switch and bash-only .bash_profile fallback - both are now subsumed by the candidate priority order. Verified: regression tests at completion-runtime.profile-path.test.ts cover zsh/fish/bash priority order, win32 PowerShell SHELL=powershell vs SHELL=pwsh selection, USERPROFILE preference, non-win32 powershell, and isCompletionInstalled behavior in tmpdir with $ZDOTDIR / $XDG_CONFIG_HOME / bash-profile-only setups. Follows clawsweeper review on openclaw#81298 (openclaw#63069): keep upstream's resolveCompletionProfilePath signature, preserve PowerShell SHELL/USERPROFILE branch, and route check/install through a single existence-aware resolver.
ca67a8b to
b8f1588
Compare
|
Rebased on latest Resolved shape (was: "I'll need to work through the new resolver shape carefully")Started from upstream Upstream API kept stable — Three new pieces:
Correctness fixes over the previous PR shape
Diff shape
Issue #63069 reproducer (macOS / zsh with @steipete — Day 8 rebase complete; ready for review when convenient. |
|
ClawSweeper PR egg: 🎁 locked until real behavior proof passes. Details
|
|
Pushed Root cause
Without FixRemoved both assertions; the function's return type ( Verification
Branch is now |
|
Cleaned up the No code change; PR head and diff are unchanged. The proof content itself is unchanged — only the field labels were normalised so the gate can see them. @clawsweeper re-review |
|
🦞🧹 I asked ClawSweeper to review this item again. Re-review progress:
|
|
Applied in P3 — Keep reload hints on the installed profile (FIXED)Setup wizard and doctor still printed hardcoded Fix: Both VerificationModule-level smoke test against the patched exports: Added regression test P2 — Preserve the bash no-profile fallback (PUSHING BACK)I think this finding is a false-positive. Tracing the actual behavior against current Before this PR ( if (shell === "bash") {
return path.join(home, ".bashrc");
}Fresh-install target: After this PR (current head if (shell === "bash") {
return [path.join(home, ".bashrc"), path.join(home, ".bash_profile")];
}
The PR does not change the fresh-install default; it adds a "use the existing profile if there is one" preference layer on top. The bot's concern ("changes fresh bash installs from If maintainers want fresh installs to prefer Branch diff vs upstream/mainPlus the existing PR changes to Ready for re-review on |
|
Heads up: this PR needs to be updated against current |
…aw#63069) Shell completion install/check helpers resolved zsh, fish, and bash profile paths through a single canonical-path resolver. On macOS with $ZDOTDIR=$HOME/.config/zsh the reporter saw completion silently written to $HOME/.zshrc - a file the shell never reads when $ZDOTDIR is set, so completion never activated in new shells. The same gap applied to fish + $XDG_CONFIG_HOME, and bash check/install disagreed about .bashrc vs .bash_profile on macOS-only installs. This change extends the upstream API around resolveCompletionProfilePath rather than replacing it: - Adds exported resolveCompletionProfilePathCandidates(shell, options) returning candidate paths in priority order. zsh honors $ZDOTDIR exclusively when set (no $HOME fallback that the shell would not read); fish honors $XDG_CONFIG_HOME exclusively when set; bash returns [.bashrc, .bash_profile]; PowerShell preserves the exact upstream win32 SHELL/USERPROFILE logic. - resolveCompletionProfilePath(shell, options) remains synchronous and returns the canonical install target (first candidate). All upstream callsites keep the same signature. - New private resolveExistingCompletionProfilePath(shell, options) picks the first existing candidate so check + install always agree on the same file. Falls back to the canonical target when nothing exists yet, so first-time installs still resolve to a stable path. - isCompletionInstalled, usesSlowDynamicCompletion, and installCompletion route through the new existence-aware resolver. installCompletion drops the per-shell switch and bash-only .bash_profile fallback - both are now subsumed by the candidate priority order. Verified: regression tests at completion-runtime.profile-path.test.ts cover zsh/fish/bash priority order, win32 PowerShell SHELL=powershell vs SHELL=pwsh selection, USERPROFILE preference, non-win32 powershell, and isCompletionInstalled behavior in tmpdir with $ZDOTDIR / $XDG_CONFIG_HOME / bash-profile-only setups. Follows clawsweeper review on openclaw#81298 (openclaw#63069): keep upstream's resolveCompletionProfilePath signature, preserve PowerShell SHELL/USERPROFILE branch, and route check/install through a single existence-aware resolver.
fef8085 to
cf936a7
Compare
…aw#63069) Shell completion install/check helpers resolved zsh, fish, and bash profile paths through a single canonical-path resolver. On macOS with $ZDOTDIR=$HOME/.config/zsh the reporter saw completion silently written to $HOME/.zshrc - a file the shell never reads when $ZDOTDIR is set, so completion never activated in new shells. The same gap applied to fish + $XDG_CONFIG_HOME, and bash check/install disagreed about .bashrc vs .bash_profile on macOS-only installs. This change extends the upstream API around resolveCompletionProfilePath rather than replacing it: - Adds exported resolveCompletionProfilePathCandidates(shell, options) returning candidate paths in priority order. zsh honors $ZDOTDIR exclusively when set (no $HOME fallback that the shell would not read); fish honors $XDG_CONFIG_HOME exclusively when set; bash returns [.bashrc, .bash_profile]; PowerShell preserves the exact upstream win32 SHELL/USERPROFILE logic. - resolveCompletionProfilePath(shell, options) remains synchronous and returns the canonical install target (first candidate). All upstream callsites keep the same signature. - New private resolveExistingCompletionProfilePath(shell, options) picks the first existing candidate so check + install always agree on the same file. Falls back to the canonical target when nothing exists yet, so first-time installs still resolve to a stable path. - isCompletionInstalled, usesSlowDynamicCompletion, and installCompletion route through the new existence-aware resolver. installCompletion drops the per-shell switch and bash-only .bash_profile fallback - both are now subsumed by the candidate priority order. Verified: regression tests at completion-runtime.profile-path.test.ts cover zsh/fish/bash priority order, win32 PowerShell SHELL=powershell vs SHELL=pwsh selection, USERPROFILE preference, non-win32 powershell, and isCompletionInstalled behavior in tmpdir with $ZDOTDIR / $XDG_CONFIG_HOME / bash-profile-only setups. Follows clawsweeper review on openclaw#81298 (openclaw#63069): keep upstream's resolveCompletionProfilePath signature, preserve PowerShell SHELL/USERPROFILE branch, and route check/install through a single existence-aware resolver.
cf936a7 to
f677b9e
Compare
|
Thanks @RomneyDa — confirmed. The
Overall the head is |
…aw#63069) Shell completion install/check helpers resolved zsh, fish, and bash profile paths through a single canonical-path resolver. On macOS with $ZDOTDIR=$HOME/.config/zsh the reporter saw completion silently written to $HOME/.zshrc - a file the shell never reads when $ZDOTDIR is set, so completion never activated in new shells. The same gap applied to fish + $XDG_CONFIG_HOME, and bash check/install disagreed about .bashrc vs .bash_profile on macOS-only installs. This change extends the upstream API around resolveCompletionProfilePath rather than replacing it: - Adds exported resolveCompletionProfilePathCandidates(shell, options) returning candidate paths in priority order. zsh honors $ZDOTDIR exclusively when set (no $HOME fallback that the shell would not read); fish honors $XDG_CONFIG_HOME exclusively when set; bash returns [.bashrc, .bash_profile]; PowerShell preserves the exact upstream win32 SHELL/USERPROFILE logic. - resolveCompletionProfilePath(shell, options) remains synchronous and returns the canonical install target (first candidate). All upstream callsites keep the same signature. - New private resolveExistingCompletionProfilePath(shell, options) picks the first existing candidate so check + install always agree on the same file. Falls back to the canonical target when nothing exists yet, so first-time installs still resolve to a stable path. - isCompletionInstalled, usesSlowDynamicCompletion, and installCompletion route through the new existence-aware resolver. installCompletion drops the per-shell switch and bash-only .bash_profile fallback - both are now subsumed by the candidate priority order. Verified: regression tests at completion-runtime.profile-path.test.ts cover zsh/fish/bash priority order, win32 PowerShell SHELL=powershell vs SHELL=pwsh selection, USERPROFILE preference, non-win32 powershell, and isCompletionInstalled behavior in tmpdir with $ZDOTDIR / $XDG_CONFIG_HOME / bash-profile-only setups. Follows clawsweeper review on openclaw#81298 (openclaw#63069): keep upstream's resolveCompletionProfilePath signature, preserve PowerShell SHELL/USERPROFILE branch, and route check/install through a single existence-aware resolver.
…penclaw#63069) oxlint typescript/no-unnecessary-type-assertion flagged two 'as string' assertions on resolveCompletionProfilePathCandidates(...)[0] and candidates[0] that don't change the inferred type without noUncheckedIndexedAccess. Removing them clears check-lint without altering runtime behavior.
…openclaw#63069) Setup wizard and doctor still printed hardcoded ~/.zshrc and ~/.config/fish/config.fish even when ZDOTDIR or XDG_CONFIG_HOME redirected the canonical profile path. After install/upgrade, affected users would be told to source a file the shell never reads. Route both hint resolvers through resolveCompletionProfilePath so the displayed reload command matches the actual install target; preserve absolute PowerShell paths and ~-shortening for POSIX shells when the redirect stays inside HOME. Adds a regression test asserting the wizard note targets a ZDOTDIR outside HOME.
f677b9e to
686393f
Compare
|
Rebased onto current Note: the linked issue #63069 was auto-closed by the stale bot for inactivity ( |
|
Closing this one: the linked issue #63069 was auto-closed for inactivity and the branch has since drifted into conflict with current main. The fix (honor ZDOTDIR / XDG_CONFIG_HOME when installing shell completion) still looks valid — happy to rebase and reopen if #63069 is revived with a fresh reproducer. |
Summary
Shell completion install/check helpers hardcoded zsh, fish, and bash profile paths under
$HOME, ignoring$ZDOTDIRand$XDG_CONFIG_HOMEand disagreeing on bash.bashrcvs.bash_profile. Reporter on macOS / zsh with$ZDOTDIR=$HOME/.config/zshsaw completion silently written to$HOME/.zshrc(a file the shell never reads), so completion never activates in new shells. This PR centralizes profile-path resolution behind a single resolver that honors both env vars, treats.bashrc/.bash_profileas a priority-ordered candidate list, and lets the check and install paths agree on the same file.Root Cause
src/cli/completion-runtime.ts:129(before):getShellProfilePath()returnedpath.join(home, ".zshrc"),path.join(home, ".bashrc"),path.join(home, ".config", "fish", "config.fish")regardless of$ZDOTDIR/$XDG_CONFIG_HOME.src/cli/completion-runtime.ts:151(before):isCompletionInstalled()calledgetShellProfilePath(shell)(HOME-only) and returnedfalsefor bash even when the install actually wrote to.bash_profile.src/cli/completion-runtime.ts:215(before):installCompletion()separately hardcodedpath.join(home, ".zshrc"),path.join(home, ".bashrc")/.bash_profilefallback, andpath.join(home, ".config", "fish", "config.fish")— so when.bashrcwas absent, install wrote to.bash_profilewhile check still looked at.bashrc, causing repeatedEnable completion?prompts on everyopenclaw update.setup/update/completion --install→installCompletion()writes to one path →isCompletionInstalled()(next session) checks the OTHER path → both reportfalseso install is offered again, and the previously written source line lives in a file the shell never reads.Changes
src/cli/completion-runtime.ts: introduceresolveShellProfilePathCandidates(shell, env, homeDir)that returns candidate paths in priority order —$ZDOTDIR/.zshrcthen$HOME/.zshrcfor zsh,$XDG_CONFIG_HOME/fish/config.fishthen$HOME/.config/fish/config.fishfor fish,.bashrcthen.bash_profilefor bash. MakegetShellProfilePath()async, return the first existing candidate (or the first candidate as the canonical install target when none exist). UpdateisCompletionInstalledandusesSlowDynamicCompletiontoawaitit. Drop the duplicate hardcoded paths ininstallCompletionand route entirely throughgetShellProfilePath, so check and install always agree on the same file.src/cli/completion-runtime.profile-path.test.ts(new): 12 colocated regression tests covering$ZDOTDIRzsh priority,$XDG_CONFIG_HOMEfish priority, bash.bashrc → .bash_profileorder, whitespace-only$ZDOTDIRhandling,os.homedir()fallback whenenv.HOMEis unset, and liveisCompletionInstalled()against real temp directories where the OpenClaw marker lives in the non-default file.Net diff: +237 / −36 LOC across 2 files (1 new test file + 1 production file).
Real behavior proof
$ZDOTDIR=$HOME/.config/zsh(a common dotfile layout, often via/etc/zshenvor.zshenv) seeopenclaw completion --installwrite the source line to$HOME/.zshrcinstead of$ZDOTDIR/.zshrc, so completion never loads in their shell. Same class of bug applies to fish users with$XDG_CONFIG_HOMEset, and to bash users who only have~/.bash_profile(macOS default):isCompletionInstalled()checks.bashrcwhileinstallCompletion()falls back to.bash_profile, so everyopenclaw updatere-asksEnable completion?. Issue: [Bug]: hell completion writes to wrong profile path when ZDOTDIR or XDG_CONFIG_HOME is set #63069../openclaw-65656on Windows 11 + Node 22.14. The smoke script reads the patched production filesrc/cli/completion-runtime.tsdirectly from the worktree, slices the patchedresolveShellProfilePathCandidates+getShellProfilePathblock (offset-pinned), prints its SHA-256, then runs both functions against (a) synthetic env tables matching the reporter scenarios and (b) real temp directories on disk where the OpenClaw marker is dropped into the non-default file. No mocks. PR head:5c935bf8onMoerAI:fix/shell-completion-xdg-zdotdir.git checkout fix/shell-completion-xdg-zdotdir && node --experimental-strip-types ./smoke-pr63069.mts. The smoke usesfs.mkdtempto build a real$ZDOTDIR/.zshrcand$HOME/.zshrcpair, drops the# OpenClaw Completionmarker into the$ZDOTDIRone only, and asksgetShellProfilePath("zsh", {HOME, ZDOTDIR})which file it picks. Same for the macOS bash case where only.bash_profileexists.5c935bf8(Windows 11 + Node 22.14, patched resolver block 1961 bytes, SHA-2566d240bd88d09e8184ee26f26f4b7336d5a42940ecba6ce63e00e7fd65a31d3e6).$ZDOTDIR/.zshrcfirst whenZDOTDIRis set (Scenario A),$HOME/.zshrcwhen it is not (Scenario B),.bashrcthen.bash_profilefor bash (Scenario C), and$XDG_CONFIG_HOME/fish/config.fishfirst when set (Scenario D). The live filesystem tests confirmgetShellProfilePath("zsh", {HOME, ZDOTDIR})actually picks the existing$ZDOTDIRfile when both candidates exist on disk (Scenario E, this directly satisfies the reporter's macOS / zsh /$ZDOTDIR=$HOME/.config/zshsetup), and falls through to.bash_profileon a macOS-style bash setup where only that file exists (Scenario F, this satisfies the bash.bashrc/.bash_profilecheck/install agreement bug). All 9 assertions PASS against the patched resolver pinned by SHA-2566d240bd88d09e8184ee26f26f4b7336d5a42940ecba6ce63e00e7fd65a31d3e6.openclaw onboard/openclaw updateflow was not exercised on the contributor machine. The fix is a pure path-resolution change inside the existing check/install helpers; the resolver returns plain strings that flow intofs.readFile/fs.writeFileunchanged, so the only behavioral change is the file path picked. Maintainers may applyproof: overrideif a live macOS interactive smoke is required; the reporter's issue already contains a concrete$ZDOTDIR=~/.config/zshreproducer.Test
pnpm test src/cli/completion-runtime.profile-path.test.ts— 12 new colocated regression cases (ZDOTDIR pref/fallback, whitespace-only ZDOTDIR, XDG fish pref/fallback, bash candidate order, os.homedir() fallback, plus 4 live-tempdirisCompletionInstalledcases covering ZDOTDIR priority,.bash_profile-only macOS,$XDG_CONFIG_HOMEfish, and the no-profile false case).pnpm test src/cli/completion-cli.test.ts src/cli/completion-cli.write-state.test.ts src/wizard/setup.completion.test.ts— unchanged, exercise the broader completion CLI; the resolver refactor preserves the original behavior when neitherZDOTDIRnorXDG_CONFIG_HOMEis set and.bashrcexists, so existing tests continue to pass.Notes
installCompletion()no longer duplicates the per-shell path logic; both the check path (isCompletionInstalled,usesSlowDynamicCompletion) and the install path now sharegetShellProfilePath(), so.bashrcvs.bash_profileand$ZDOTDIR/$XDG_CONFIG_HOMEstay consistent..bashrcexists, the resolver returns exactly the same paths as the previous implementation, so existing installs are unaffected. Powershell paths are unchanged.src/commands/doctor-completion.ts:111and:161print hardcoded reload-hint strings (source ~/.zshrcetc.). Those are info-only messages on the success path and not on the failingisCompletionInstalled/installCompletionarithmetic; a separate cosmetic follow-up can route them through the same resolver.Closes #63069