Skip to content

fix(shell): stop the profile block baking in a snapshot of PATH - #131

Merged
GordonBeeming merged 5 commits into
mainfrom
gb/fix-profile-path-snapshot
Aug 13, 2026
Merged

fix(shell): stop the profile block baking in a snapshot of PATH#131
GordonBeeming merged 5 commits into
mainfrom
gb/fix-profile-path-snapshot

Conversation

@GordonBeeming

@GordonBeeming GordonBeeming commented Aug 13, 2026

Copy link
Copy Markdown
Owner

Summary

__copilot_update_profile in copilot_here.sh wrote the shell profile block with an unquoted heredoc delimiter, so $HOME and $PATH expanded while the file was being written. Profiles ended up holding the literal value of PATH from install time, and every shell start after that replaced the live PATH with that copy instead of prepending ~/.local/bin to it. On WSL that throws away the Windows interop entries, which is why code . stopped resolving in #128.

I reproduced the reported block byte for byte before changing anything, and confirmed origin/main still carries the bug at copilot_here.sh:235.

Two things worth calling out:

  • The case ":$PATH:" guard word was frozen too, so it tested the install-time PATH rather than the current one. On a second install the snapshot already contains ~/.local/bin, the guard matches, and the export never runs. That is why this doesn't affect everyone.
  • The C# writer (EnsureSourcedInUnixProfile) always wrote the literal $HOME/$PATH and was never the problem. But install.sh runs --update (the broken bash writer) before --install-shells, and EnsureBlock returned early whenever it saw the start marker, so the correct writer could never repair the damage.

Changes

  • copilot_here.sh: quote the heredoc delimiter, and write the markers and script path literally. The output is now byte-identical to the block ShellIntegration writes.
  • app/Infrastructure/ShellIntegration.cs: EnsureBlock compares the existing marked region against the block it wants and rewrites on drift. Config above and below the markers is untouched, and a start marker with no matching end is left alone so it can't swallow the rest of the file.
  • tests/integration/test_cli.sh: regression test for the profile block, plus a fix for cleanup(), which built paths from $BASH_SOURCE after the tests had changed directory and so never removed publish/cli-test.
  • tests/CopilotHere.UnitTests/ShellIntegrationTests.cs: cases for the new EnsureBlock behaviour, including BOM preservation.

Review found four more problems in the first version of this branch, all fixed here:

  • __copilot_update downloaded the new script but called __copilot_update_profile before sourcing it, so the first update after a broken release still ran the old in-memory writer and re-baked the snapshot. The reload now happens before the profile writes. Without this, healing an affected machine would have taken two updates rather than one.
  • EnsureBlock's rewrite path used the File.WriteAllText overload that emits BOM-less UTF-8. install.ps1 writes the PowerShell profile as UTF-8 with a BOM, which Windows PowerShell 5.1 needs to avoid falling back to the legacy code page, so a rewrite would have stripped it and garbled any non-ASCII content in the profile. The rewrite now detects the original BOM state and preserves it, across UTF-8, UTF-16 and UTF-32.
  • A BOM-less profile saved in a legacy code page is not valid UTF-8, so File.ReadAllText substitutes U+FFFD and writing that text back destroyed the original bytes. A CP1252 é (0xE9) came out as EF BF BD. EnsureBlock now leaves such a file untouched. Identifying which legacy code page it is remains undecidable, so the trade is deliberate: a stale block in a profile we cannot read stays unrepaired rather than being rewritten into garbage.
  • The first attempt at that guard tested for U+FFFD in the decoded text, which was wrong, because U+FFFD is a legal character to write in a file. A valid UTF-8 profile containing one was refused, so the block was silently not installed while the caller still reported success. The readability test is now a strict UTF-8 decode of the bytes: a valid profile passes whatever characters it contains, and only genuinely invalid bytes are treated as unreadable.

Anyone already affected gets healed by a single copilot_here --update once this ships, with no manual .bashrc editing.

Test plan

  • dotnet test tests/CopilotHere.UnitTests: 620 passed, 0 failed.
  • dotnet test tests/CopilotHere.IntegrationTests: 5 passed, 0 failed.
  • bash tests/integration/test_cli.sh: 12 passed, 0 failed.
  • Reverted the heredoc fix on purpose and re-ran. The new test fails with the sentinel PATH baked into the profile; restoring the fix makes it pass again, so the test actually guards the bug.
  • Ran the built binary's --install-shells against a throwaway $HOME holding the corrupted block. It rewrote the block in place and kept the surrounding config.
  • Diffed the shell-written block against the C# block to confirm they match byte for byte.
  • Reproduced the legacy-code-page corruption in a scratch console app before fixing it, and confirmed the new test fails with the guard removed. It asserts on raw bytes rather than decoded text, since a text-level assertion passes while the corruption still happens.

Closes #128

Summary by CodeRabbit

  • Bug Fixes

    • Profile updates now preserve literal $HOME and $PATH references, preventing incorrect installation-time substitutions.
    • Marked configuration blocks are updated when stale while preserving surrounding content.
    • Malformed blocks remain unchanged for safer configuration handling.
  • Tests

    • Added coverage for updating, preserving, appending, and safely handling configuration blocks.
    • Added integration coverage for profile updates and PATH behavior.

__copilot_update_profile wrote the marker block with an unquoted heredoc, so $HOME
and $PATH expanded while the file was being written. Profiles ended up holding the
value of PATH from install time, and every shell start replaced the live PATH with
that copy instead of prepending ~/.local/bin to it. On WSL that drops the Windows
interop entries, so `code .` stops resolving.

Quoting the delimiter and writing the markers and script path literally makes the
shell-written block byte-identical to the one ShellIntegration writes.

EnsureBlock returned as soon as it saw the start marker, so --install-shells could
never repair a block an older release had corrupted. It now compares the marked
region against the block it wants and rewrites on drift, leaving the surrounding
config alone and leaving a marker with no matching end untouched.

Also fixes cleanup() in the CLI integration tests, which built paths from
$BASH_SOURCE after the tests had changed directory and so never removed
publish/cli-test.
@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 9a55abd8-7caf-4ec6-b83c-66afc1c5611f

📥 Commits

Reviewing files that changed from the base of the PR and between d59cd5e and 4867a68.

📒 Files selected for processing (4)
  • app/Infrastructure/ShellIntegration.cs
  • copilot_here.sh
  • tests/CopilotHere.UnitTests/ShellIntegrationTests.cs
  • tests/integration/test_cli.sh

📝 Walkthrough

Walkthrough

EnsureBlock now reconciles existing profile blocks and protects malformed content. Profile generation now preserves literal $HOME and $PATH references. Unit and integration tests cover block replacement, malformed blocks, missing markers, and PATH preservation.

Changes

Profile integrity

Layer / File(s) Summary
Marked block reconciliation and tests
app/Infrastructure/ShellIntegration.cs, tests/CopilotHere.UnitTests/ShellIntegrationTests.cs
EnsureBlock is now accessible to tests. It rewrites stale complete blocks, preserves current blocks, leaves malformed blocks unchanged, and appends missing blocks.
Literal profile generation and integration validation
copilot_here.sh, tests/integration/test_cli.sh
Profile generation retains literal $HOME and $PATH expressions and sources $HOME/.copilot_here.sh. Integration coverage verifies existing content and PATH values remain intact.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Mergeability Score: ⚪ Minimal · up to 4867a

The profile-generation fix and repair behavior are covered by targeted regression tests, and no actionable merge-blocking risk remains beyond normal checks and review.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Linked Issues check ✅ Passed The changes preserve the live PATH, add ~/.local/bin, and provide regression tests for issue #128.
Out of Scope Changes check ✅ Passed The changes remain within scope: profile generation, marked-block reconciliation, regression tests, and related test cleanup.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main fix: preventing the shell profile block from storing an installation-time PATH snapshot.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch gb/fix-profile-path-snapshot

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@GordonBeeming

Copy link
Copy Markdown
Owner Author

@codex review

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Fixes a shell profile regression where __copilot_update_profile wrote a PATH snapshot (expanding $HOME/$PATH at write time) into .bashrc/.zshrc, causing subsequent shells—especially on WSL—to lose dynamic PATH entries (e.g., Windows interop) and breaking commands like code .. The PR makes the shell-writer emit the same literal $HOME/$PATH block as the .NET ShellIntegration writer, and updates the .NET writer to heal already-corrupted blocks by reconciling drift inside the marked region.

Changes:

  • copilot_here.sh: quote the heredoc delimiter and emit the marker block (including $HOME/$PATH) literally to prevent install-time expansion.
  • ShellIntegration.EnsureBlock: rewrite the marked region if it differs from the desired block (while preserving surrounding user config and avoiding unsafe rewrites when the end marker is missing).
  • Tests: add a regression integration test to ensure the profile block keeps literal $PATH, and add unit tests covering the new EnsureBlock behavior.

Reviewed changes

Copilot reviewed 4 out of 4 changed files in this pull request and generated no comments.

File Description
copilot_here.sh Prevents install-time expansion by writing the profile heredoc block with a quoted delimiter and literal $HOME/$PATH.
app/Infrastructure/ShellIntegration.cs Updates EnsureBlock to detect and repair stale/incorrect marked blocks without touching surrounding user content.
tests/integration/test_cli.sh Adds an end-to-end regression test ensuring profile updates don’t bake PATH snapshots, plus fixes cleanup path resolution.
tests/CopilotHere.UnitTests/ShellIntegrationTests.cs Adds focused unit coverage for EnsureBlock rewrite/append/no-op cases, including missing end-marker safety.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4867a68586

ℹ️ 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".

Comment thread copilot_here.sh
Comment thread app/Infrastructure/ShellIntegration.cs Outdated
@GordonBeeming

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown
✅ Action performed

Review finished.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

…ewrite

Address Codex review feedback on PR #131 (round 1):

- __copilot_update downloaded the fixed copilot_here.sh but still called the
  in-memory (pre-update) __copilot_update_profile before sourcing the new
  file, so a user upgrading from a release with the PATH-snapshot bug would
  have it re-applied by the update that was supposed to fix it. Reload the
  freshly downloaded script first so the repaired function is the one that
  runs.

- ShellIntegration.EnsureBlock rewrote a stale marked region with
  File.WriteAllText's default (BOM-less UTF-8) overload. install.ps1 writes
  the PowerShell profile as UTF-8 with a BOM so Windows PowerShell 5.1
  recognizes the encoding; losing the BOM on rewrite would make a profile
  with non-ASCII content reinterpreted via the legacy code page and garbled.
  Detect the original BOM before reading and carry it through the write.
@GordonBeeming

Copy link
Copy Markdown
Owner Author

@codex review

@GordonBeeming
GordonBeeming requested a lite review from Copilot August 13, 2026 15:51
@GordonBeeming

Copy link
Copy Markdown
Owner Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented Aug 13, 2026

Copy link
Copy Markdown
⚠️ Action not completed

Review rate limited.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: d3f9ef1f98

ℹ️ 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".

Comment thread app/Infrastructure/ShellIntegration.cs Outdated
…le rewrite

Address Codex round-2 feedback on PR #131: the previous fix only checked for
the 3-byte UTF-8 BOM before rewriting a stale marked region, so a profile
that predates copilot_here and is already UTF-16 (Notepad's "Unicode" save
option and PowerShell ISE both default to it) or UTF-32 would still get
silently collapsed to BOM-less UTF-8 on rewrite. File.ReadAllText already
auto-detects and decodes all of these correctly - verified empirically
against a UTF-16LE file - it just doesn't report which encoding it found.
DetectBomEncoding replaces the UTF-8-only check and covers UTF-8, UTF-16LE/BE,
and UTF-32LE/BE, checking the longer UTF-32 preambles first since UTF-32LE's
BOM starts with the same two bytes as UTF-16LE's. Added a UTF-16 regression
test alongside the existing UTF-8-BOM and no-BOM cases.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

Suppressed comments (2)

app/Infrastructure/ShellIntegration.cs:673

  • FileStartsWithUtf8Bom only detects the UTF-8 BOM. If a PowerShell profile starts with a UTF-16/UTF-32 BOM, EnsureBlock will treat it as BOM-less and rewrite the file as BOM-less UTF-8, which can reintroduce the same PowerShell 5.1 encoding problem this change is trying to avoid. Consider treating any Unicode BOM as “has BOM” for purposes of re-emitting a BOM on rewrite.
    using var stream = File.OpenRead(filePath);
    Span<byte> preamble = stackalloc byte[3];
    var read = stream.ReadAtLeast(preamble, preamble.Length, throwOnEndOfStream: false);
    return read == 3 && preamble[0] == 0xEF && preamble[1] == 0xBB && preamble[2] == 0xBF;
  }

app/Infrastructure/ShellIntegration.cs:615

  • The comment says "install.ps1 writes the PowerShell profile as UTF-8 with a BOM", but install.ps1 doesn’t write the PowerShell profile content (it only downloads/sources copilot_here.ps1 and runs commands). This looks like it should reference the PowerShell profile updater (e.g., copilot_here.ps1 / Update-ProfileWithMarkers) to avoid misleading future maintainers about where the BOM originates.
    // install.ps1 writes the PowerShell profile as UTF-8 with a BOM (Windows PowerShell 5.1
    // needs the BOM to recognize UTF-8; without it, a profile with non-ASCII content is
    // reinterpreted using the legacy code page and comes out garbled). File.ReadAllText
    // strips the BOM on decode without recording that it was there, and the WriteAllText
    // overload below defaults to BOM-less UTF-8, so a rewrite would silently drop it unless

@GordonBeeming

Copy link
Copy Markdown
Owner Author

@codex review

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1828682642

ℹ️ 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".

Comment thread app/Infrastructure/ShellIntegration.cs

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 4 out of 4 changed files in this pull request and generated no new comments.

Suppressed comments (1)

app/Infrastructure/ShellIntegration.cs:651

  • EnsureBlock() detects the original profile encoding (via BOM) and preserves it on the rewrite path, but the no-marker path immediately below uses File.AppendAllText without specifying encoding. If the existing profile is UTF-16/UTF-32 or UTF-8-with-BOM, appending with the default UTF-8 writer can corrupt the file by mixing encodings. Use the AppendAllText overload that takes an Encoding (or rewrite via WriteAllText) so the appended block uses the detected encoding too.
      return;
    }

A BOM-less profile saved in a legacy code page is not valid UTF-8, so
File.ReadAllText substitutes U+FFFD for its non-ASCII bytes. Writing that text
back baked the loss in permanently: a CP1252 `é` (0xE9) came out as EF BF BD and
the original byte was gone. The drift rewrite is what made this reachable, since
the previous early-return never rewrote an existing block at all.

Detecting the actual code page is undecidable, but detecting that the decode
failed is not: U+FFFD cannot come out of a clean decode, so its presence is a
reliable signal. EnsureBlock now returns without touching such a file, which
also covers the append path. The trade is deliberate: a stale block in a profile
we cannot read is left unrepaired rather than rewritten into garbage.
@GordonBeeming
GordonBeeming marked this pull request as ready for review August 13, 2026 16:08

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 04cfe8af93

ℹ️ 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".

Comment thread app/Infrastructure/ShellIntegration.cs Outdated
…U+FFFD

The previous guard treated any U+FFFD in the decoded text as proof the read had
failed. That is wrong: U+FFFD is a legal character to write in a file, so a valid
UTF-8 profile containing one deliberately was refused, and EnsureBlock returned
without installing or repairing the block while the caller still reported success.

Decoding strictly is the accurate test. A BOM-less file is now run through a
UTF-8 decode that throws on invalid bytes: a valid profile passes whatever
characters it contains, and legacy code page bytes throw and leave the file
untouched as before.
@GordonBeeming
GordonBeeming merged commit 523b95e into main Aug 13, 2026
37 of 56 checks passed
@GordonBeeming
GordonBeeming deleted the gb/fix-profile-path-snapshot branch August 13, 2026 16:37
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.

copilot_here overwrites PATH destroying the existing PATH

2 participants