Skip to content

fix(lifecycle): gate hook auto-install on the running process, not disk presence (#1044) - #1046

Merged
github-actions[bot] merged 2 commits into
mainfrom
fix/issue-1044-uv-tool-running-process
Jul 1, 2026
Merged

fix(lifecycle): gate hook auto-install on the running process, not disk presence (#1044)#1046
github-actions[bot] merged 2 commits into
mainfrom
fix/issue-1044-uv-tool-running-process

Conversation

@robotrocketscience

@robotrocketscience robotrocketscience commented Jul 1, 2026

Copy link
Copy Markdown
Owner

Fixes #1044.

Problem

The #834 guard (a worktree's uv run aelf must not rewrite the user's global ~/.claude/settings.json hooks) was inert for any user who also had a uv tool install. lifecycle._is_uv_tool_install() short-circuits True on a filesystem-presence check — ~/.local/share/uv/tools/aelfrice/ existing anywhere on the box — before the correct sys.prefix-under-tools-root check runs. So a source worktree (whose sys.prefix is its own venv, not the uv-tool env) was misclassified as the uv-tool install, and every aelf invocation from the tree silently rewrote the global hooks backwards (observed v3.8.0 → v3.6.0, 8 default-on hooks reset).

Fix

  1. Gate on the running process, not disk presence. New lifecycle._running_from_uv_tool() returns True iff sys.prefix/sys.executable resolves under the uv tools root. auto_install.is_running_from_uv_tool_install() now delegates to it. _is_uv_tool_install() keeps its exists-or-running semantics for upgrade_advice(), which legitimately asks "is a uv-tool install present on this box?" (the two callers want different questions, so they no longer share one predicate).
  2. Defense in depth — never downgrade. maybe_install_manifest now skips the merge (non-force path) when the running version is older than the on-disk stamp, emitting a one-line notice instead of stamping backwards. An explicit aelf setup (force=True) still re-stamps for a genuine downgrade.

Tests

  • test_running_from_uv_tool_distinguishes_process_from_presence — the exact repro: uv-tool dir exists on disk but sys.prefix is a worktree venv → _is_uv_tool_install() True (presence), _running_from_uv_tool() False (process), gate stays False.
  • test_running_from_uv_tool_true_under_tools_root — process under tools root → True.
  • test_maybe_install_manifest_never_downgrades — older running version leaves stamp + settings untouched.
  • Updated the delegation test to the new seam. Full affected suites pass (111 tests).

🤖 Generated with Claude Code

Summary by Sourcery

Guard hook auto-installation based on the currently running uv-tool process instead of mere disk presence, and prevent non-forced runs from downgrading previously installed hook versions.

Bug Fixes:

  • Ensure worktree uv run aelf invocations no longer rewrite or downgrade the user's globally installed hooks when a separate uv-tool install exists on disk.
  • Fix uv-tool detection by distinguishing between the running process being under the uv tools root and a uv-tool install merely existing on the filesystem.
  • Prevent maybe_install_manifest from downgrading the on-disk hook version when a lower version of the tool is running, unless explicitly forced via setup.

Enhancements:

  • Refactor lifecycle uv-tool detection into separate predicates for process-based gating and filesystem-based presence checks to better support upgrade advice logic.

Documentation:

  • Document the hook downgrade regression and its fix in the v3 changelog, including the new process-based gating and never-downgrade behavior.

Tests:

  • Add regression tests covering process-vs-presence uv-tool detection, the auto-install gate behavior, and the never-downgrade manifest installation guard.

Summary by CodeRabbit

  • Bug Fixes

    • Fixed auto-install behavior so running from a source worktree no longer overwrites existing hook settings.
    • Added protection to prevent older versions from downgrading installed configuration during non-forced runs.
    • Improved detection of when the app is actually running from a uv-managed environment, reducing false matches from files left on disk.
  • Tests

    • Expanded regression coverage for environment detection and the no-downgrade safeguard.

…sk presence (#1044)

_is_uv_tool_install() short-circuited True on a filesystem-presence check
(~/.local/share/uv/tools/aelfrice/ existing anywhere on the box) before the
correct sys.prefix-under-tools-root check ran. So a source worktree's
`uv run aelf` was misclassified as the uv-tool install and silently
rewrote ~/.claude/settings.json hooks backwards (v3.8.0 -> v3.6.0),
reintroducing #834 for any user who also had a uv-tool install.

Split the predicate: the auto-install gate now delegates to a new
lifecycle._running_from_uv_tool() (running-process check only), while
_is_uv_tool_install() keeps disk-presence semantics for upgrade_advice().
Defense in depth: maybe_install_manifest refuses to stamp the hook surface
backwards in the non-force path (skips with a notice; `aelf setup` still
re-stamps). Regression tests cover the presence-vs-process split and the
never-downgrade guard.
@sourcery-ai

sourcery-ai Bot commented Jul 1, 2026

Copy link
Copy Markdown

Reviewer's Guide

Fixes incorrect classification of worktree invocations as uv-tool installs and prevents automatic hook manifest downgrades by splitting uv-tool detection into process-vs-presence and adding a version-based guard in the auto-install path, with corresponding tests and changelog updates.

File-Level Changes

Change Details Files
Gate hook auto-install on the running process instead of uv-tool disk presence and add a downgrade-prevention guard in the manifest installer.
  • Change auto-install gate to delegate to lifecycle._running_from_uv_tool instead of _is_uv_tool_install, ensuring only the actual uv-tool-managed process triggers auto-install.
  • Introduce helper functions _version_key, _is_downgrade, and _downgrade_skip_result to compare versions and return a structured result when a downgrade would occur.
  • Update maybe_install_manifest to skip non-force installs when the running version is older than the stamped version, returning a descriptive message and leaving hooks/settings untouched.
  • Expand the is_running_from_uv_tool_install docstring to clarify process-based gating semantics and previous regression behavior.
src/aelfrice/auto_install.py
Split uv-tool detection into a process-based checker and a box-level presence checker to serve different call sites correctly.
  • Add lifecycle._running_from_uv_tool to determine if the current process’s sys.prefix/sys.executable lives under the uv tools root (~/.local/share/uv/tools/).
  • Refactor _is_uv_tool_install to answer the presence question (install dir exists or current process is under uv tools root) and document its intended use for upgrade_advice only.
  • Ensure _running_from_uv_tool is the canonical process-level predicate reused by other logic.
src/aelfrice/lifecycle.py
Add regression tests for process-vs-presence detection and downgrade prevention, and adjust existing tests to the new seams.
  • Update the delegation test to assert that is_running_from_uv_tool_install delegates to lifecycle._running_from_uv_tool and covers both True/False branches.
  • Add tests that distinguish uv-tool disk presence from the running process location, ensuring worktree venvs are not misclassified as uv-tool installs.
  • Add a test to verify that maybe_install_manifest never downgrades: older running versions skip merges and preserve stamps/settings.
  • Wire in sys usage for tests and set up temporary HOME, prefixes, and executables to simulate different environments.
tests/test_auto_install.py
Document the regression fix and behavior changes in the changelog.
  • Add a Fixed entry describing the worktree uv run downgrade issue, the split between _running_from_uv_tool and _is_uv_tool_install, and the new never-downgrade behavior in maybe_install_manifest.
CHANGELOG/v3.md

Assessment against linked issues

Issue Objective Addressed Explanation
#1044 Update uv-tool detection so the hook auto-install gate checks whether the current running process is the uv-tool install (via sys.prefix/sys.executable under the uv tools root), rather than merely checking for the existence of a uv-tool install on disk.
#1044 Add a defense-in-depth mechanism in hook auto-install (maybe_install_manifest/auto_install_at_cli_entry) to prevent downgrading hooks: if the running aelfrice version is older than the version stamped on disk, skip the merge and emit a notice instead of silently rewriting hooks backwards.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown

PR-size soft cap

This PR is over the advisory size threshold:

  • 213 changed lines (limit: 200)
  • 4 changed files (limit: 3)

Bigger PRs collide with more open work, which under the parallel-session workflow tends to produce repeated attn:merge-conflict cycles (see #602). When practical, split into smaller PRs that each touch a focused surface.

This is advisory only — nothing is blocked. If the size is intentional (large refactor, module removal, generated code), apply the size:override label and this comment will be removed on the next push.

@coderabbitai

coderabbitai Bot commented Jul 1, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@robotrocketscience, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 35 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 3fe6be01-3d1b-4e90-9c83-66c2b4c0c68f

📥 Commits

Reviewing files that changed from the base of the PR and between 5feeea9 and eed247b.

📒 Files selected for processing (2)
  • src/aelfrice/lifecycle.py
  • tests/test_auto_install.py
📝 Walkthrough

Walkthrough

Splits uv-tool detection into process-based (_running_from_uv_tool) versus disk-presence-based checks, and updates the auto-install gate to use process detection. Adds a version-downgrade guard in maybe_install_manifest that skips merges when the running version is older than the stamp. Includes regression tests and a changelog entry.

Changes

Downgrade guard and detection fix

Layer / File(s) Summary
Process-based uv-tool detection
src/aelfrice/lifecycle.py
Adds _running_from_uv_tool() checking sys.prefix/sys.executable against the uv tools root; refactors _is_uv_tool_install() to use disk presence first, then process detection as secondary signal.
Auto-install gate wiring
src/aelfrice/auto_install.py
is_running_from_uv_tool_install() now delegates to lifecycle._running_from_uv_tool() instead of the presence-based check, with updated docs/comments.
Never-downgrade guard
src/aelfrice/auto_install.py
Adds version-parsing/comparison helpers and a downgrade-detection result, applied both before and after the stamp lock in maybe_install_manifest to skip merges from older running versions.
Regression tests and changelog
tests/test_auto_install.py, CHANGELOG/v3.md
Adds tests distinguishing process-based vs disk-presence uv-tool detection and verifying downgrade-skip behavior; documents the fix in the changelog.

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

Sequence Diagram(s)

sequenceDiagram
    participant CLI as aelf CLI entry
    participant Gate as auto_install.is_running_from_uv_tool_install
    participant Lifecycle as lifecycle._running_from_uv_tool
    participant Installer as maybe_install_manifest
    participant Stamp as version stamp file
    participant Settings as ~/.claude/settings.json

    CLI->>Gate: check if auto-install should run
    Gate->>Lifecycle: query running process location
    Lifecycle-->>Gate: true/false (process under uv tools root)
    Gate-->>CLI: gate result
    CLI->>Installer: maybe_install_manifest(installed_version)
    Installer->>Stamp: read prev_version
    Installer->>Installer: is_downgrade(installed_version, prev_version)
    alt is downgrade and not forced
        Installer-->>CLI: skip result, "not downgrading"
    else safe to proceed
        Installer->>Settings: merge hook entries
        Installer->>Stamp: write new version
        Installer-->>CLI: success result
    end
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly summarizes the main fix: gating hook auto-install on the running process instead of disk presence.
Description check ✅ Passed It explains the bug, fix, and tests well, with the linked issue called out, though the template headings are not fully followed.
Linked Issues check ✅ Passed The PR implements the requested process-based gate, preserves disk-presence semantics for advice, and adds the no-downgrade guard.
Out of Scope Changes check ✅ Passed Changes stay focused on the hook-gating bug, the downgrade guard, tests, and the changelog entry.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/issue-1044-uv-tool-running-process

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.

@sourcery-ai sourcery-ai 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.

Hey - I've found 1 issue, and left some high level feedback:

  • The downgrade guard in maybe_install_manifest is evaluated twice (once before acquiring the lock and once after re-reading the stamp); consider consolidating this into a single post-lock check to avoid duplication and ensure the decision is always based on the up-to-date stamp.
  • The logic for deriving the uv tools root (Path.home() / '.local' / 'share' / 'uv' / 'tools' with replace('\', '/')) is duplicated in _running_from_uv_tool and _is_uv_tool_install; factoring this into a shared helper would reduce repetition and keep future changes to the path semantics in one place.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The downgrade guard in `maybe_install_manifest` is evaluated twice (once before acquiring the lock and once after re-reading the stamp); consider consolidating this into a single post-lock check to avoid duplication and ensure the decision is always based on the up-to-date stamp.
- The logic for deriving the uv tools root (`Path.home() / '.local' / 'share' / 'uv' / 'tools'` with `replace('\', '/')`) is duplicated in `_running_from_uv_tool` and `_is_uv_tool_install`; factoring this into a shared helper would reduce repetition and keep future changes to the path semantics in one place.

## Individual Comments

### Comment 1
<location path="src/aelfrice/lifecycle.py" line_range="335-344" />
<code_context>
+def _running_from_uv_tool() -> bool:
</code_context>
<issue_to_address>
**issue (bug_risk):** Using string `startswith` on normalized paths can mis-detect when one path is a sibling rather than a true descendant of the uv tools root.

`candidate.replace("\\", "/").startswith(uv_tools_root)` can incorrectly return True for siblings like `/home/me/.local/share/uv/toolshed/...` when `uv_tools_root` is `/home/me/.local/share/uv/tools`, causing non‑uv environments to be treated as uv-tool installs. Instead, use a path-based containment check such as `Path(candidate).resolve().is_relative_to(Path(uv_tools_root))` (3.9+) or `os.path.commonpath([candidate, uv_tools_root]) == uv_tools_root` for a correct descendant test.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment thread src/aelfrice/lifecycle.py

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🧹 Nitpick comments (1)
tests/test_auto_install.py (1)

425-445: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Consider adding a companion test for the force=True override path.

This test only covers the non-force skip. The PR objectives explicitly state explicit aelf setup/upgrade flows must still be able to restamp even when it would be a downgrade — that behavior isn't exercised here. Adding a force=True case would close the loop on this guard's full contract.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/test_auto_install.py` around lines 425 - 445, The current test only
covers the non-force downgrade guard in maybe_install_manifest, so add a
companion assertion for the force override path. Extend
test_maybe_install_manifest_never_downgrades (or add a nearby test) to call
auto_install.maybe_install_manifest with force=True and verify that it does
restamp and update settings even when installed_version is older. Keep the
existing symbols auto_install.maybe_install_manifest, auto_install.read_stamp,
and write_stamp as the anchor points for the new coverage.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/aelfrice/auto_install.py`:
- Around line 359-382: The version comparison in _version_key() and
_is_downgrade() is using a custom tuple parser that does not follow PEP 440, so
prereleases and normalized versions can be misordered. Replace the current logic
in these helpers with packaging.version.Version comparisons so installed_version
and prev are evaluated with proper PEP 440 semantics, while still preserving the
_UNSTAMPED sentinel check in _is_downgrade().

In `@src/aelfrice/lifecycle.py`:
- Around line 352-357: The `_running_from_uv_tool()` check is using string
prefix matching, which can falsely match paths like uv tools worktrees; update
this logic to resolve `sys.prefix` and `sys.executable` and use the existing
path containment helper instead of `startswith()`. Keep the `uv_tools_root`
guard, but route the `candidate` checks through the containment function so only
the actual uv tools install is treated as true.

---

Nitpick comments:
In `@tests/test_auto_install.py`:
- Around line 425-445: The current test only covers the non-force downgrade
guard in maybe_install_manifest, so add a companion assertion for the force
override path. Extend test_maybe_install_manifest_never_downgrades (or add a
nearby test) to call auto_install.maybe_install_manifest with force=True and
verify that it does restamp and update settings even when installed_version is
older. Keep the existing symbols auto_install.maybe_install_manifest,
auto_install.read_stamp, and write_stamp as the anchor points for the new
coverage.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 5bd78429-9a23-4454-8400-70cf354ca931

📥 Commits

Reviewing files that changed from the base of the PR and between 6ca4c1e and 5feeea9.

📒 Files selected for processing (4)
  • CHANGELOG/v3.md
  • src/aelfrice/auto_install.py
  • src/aelfrice/lifecycle.py
  • tests/test_auto_install.py

Comment thread src/aelfrice/auto_install.py
Comment thread src/aelfrice/lifecycle.py
@robotrocketscience

Copy link
Copy Markdown
Owner Author

[claim:review:Setr:2026-07-01T23:32:54Z]

@robotrocketscience

Copy link
Copy Markdown
Owner Author

Reviewed — LGTM, merging.

Both parts of the issue's suggested fix land cleanly:

  • Predicate split: gate now delegates to _running_from_uv_tool() (running-process check via sys.prefix/sys.executable under the uv tools root), while _is_uv_tool_install() keeps disk-presence semantics for upgrade_advice(). The two callers ask different questions and no longer share one predicate — this is what fixes the [bug] uv run aelf from a worktree silently mutates ~/.claude/settings.json and opts user out of hooks #834 regression for users who also have a uv-tool install.
  • Never-downgrade defense-in-depth: _is_downgrade() uses int-tuple version keys (avoids the "3.10" < "3.9" string-compare trap), and the guard is re-checked after the under-lock stamp re-read, so it's TOCTOU-safe. force=True (explicit aelf setup) correctly bypasses it.

Regression coverage is on point (process-vs-presence split, under-root true case, never-downgrade). FF-clean on main, single signed commit, all CI green.

@robotrocketscience robotrocketscience added the ready-to-merge Trigger merge-train: FF main to this PR's head label Jul 1, 2026
@robotrocketscience

Copy link
Copy Markdown
Owner Author

[release:review:Setr:2026-07-01T23:34:35Z]

@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown

merge-train: blocked

2 review thread(s) are unresolved on these files: src/aelfrice/auto_install.py, src/aelfrice/lifecycle.py. Resolve them on the PR (click 'Resolve conversation' on each) and re-add the label.

The ready-to-merge label has been removed. Address the issue above and re-add the label when you're ready for another attempt.

@github-actions github-actions Bot removed the ready-to-merge Trigger merge-train: FF main to this PR's head label Jul 1, 2026
…1044 review)

_running_from_uv_tool matched sys.prefix/sys.executable against the uv
tools root with a bare startswith, so a sibling like .../uv/toolshed
would satisfy the prefix test against .../uv/tools and be misread as the
uv-tool install. Anchor the comparison with a trailing separator so only
true descendants match. Regression test covers the sibling case.
@robotrocketscience

Copy link
Copy Markdown
Owner Author

Review-thread dispositions (reviewer: unblocking for merge):

Path-containment (Sourcery + CodeRabbit, `lifecycle.py`) — valid, fixed in `eed247b8`. `_running_from_uv_tool()` now anchors the uv-tools-root comparison with a trailing separator, so siblings like `.../uv/toolshed` and `.../uv/tools-worktree` no longer satisfy the prefix test. Added a regression test for the sibling case.

PEP 440 version comparison (CodeRabbit, `auto_install.py`) — declined, with reason:

  • `packaging` is not a declared dependency here — it is transitive-only in `uv.lock` and imported nowhere in `src/`. Importing `packaging.version` directly would either require declaring a new runtime dep (a posture change this project avoids) or trip `deptry`'s imported-but-not-declared check.
  • `_version_key` is a deliberate, documented stdlib choice ("deliberately not full PEP 440 — it only needs to answer 'is A older than B' for the never-downgrade guard").
  • The flagged trigger (prerelease/partial-version stamps such as `3.8.0rc1` or `3.8`) does not occur: aelfrice stamps the running `X.Y.Z` release version, and releases are full three-part versions. The guard is also defense-in-depth behind a primary gate that already excludes worktrees.

Resolving all three threads.

@robotrocketscience robotrocketscience added the ready-to-merge Trigger merge-train: FF main to this PR's head label Jul 1, 2026
@github-actions
github-actions Bot merged commit eed247b into main Jul 1, 2026
28 checks passed
@github-actions github-actions Bot removed the ready-to-merge Trigger merge-train: FF main to this PR's head label Jul 1, 2026
@github-actions

github-actions Bot commented Jul 1, 2026

Copy link
Copy Markdown

merge-train: merged eed247bmain via FF push.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant