Skip to content

fix(hooks): stop lint.sh from modifying or discarding working tree changes - #1070

Open
k21993 wants to merge 1 commit into
huggingface:mainfrom
k21993:fix/lint-hook-no-mutate
Open

fix(hooks): stop lint.sh from modifying or discarding working tree changes#1070
k21993 wants to merge 1 commit into
huggingface:mainfrom
k21993:fix/lint-hook-no-mutate

Conversation

@k21993

@k21993 k21993 commented Aug 7, 2026

Copy link
Copy Markdown

Summary

lint.sh currently formats in place and then runs git checkout -- $CHANGED to undo the formatting. That restores from HEAD, so if a touched file also has uncommitted edits, those edits are discarded along with the formatting changes.

The hook is run by contributor workflows such as alignment and pre-submit checks, so this can happen without the author explicitly choosing to format files.

The same step also leaves the tree dirty. The restore filtered to *.py, but this ruff version formats Python code blocks inside Markdown, so env README.md files were rewritten and never restored. In practice that shows up as roughly 31 modified READMEs after every run.

The fix is to ask the tools for status instead of writing: usort check and ruff format --check. Nothing is modified, so nothing needs restoring, and both problems go away at the root.

-uv run usort format src/ tests/ >/dev/null 2>&1
-uv run ruff format src/ tests/ envs/ >/dev/null 2>&1
-
-CHANGED=$(git diff --name-only -- '*.py' 2>/dev/null || true)
-if [ -n "$CHANGED" ]; then
-    ...
-    git checkout -- $CHANGED 2>/dev/null || true
-    exit 1
-fi
+FORMAT_FAILED=0
+uv run usort check src/ tests/ || FORMAT_FAILED=1
+uv run ruff format --check src/ tests/ envs/ || FORMAT_FAILED=1
+
+if [ "$FORMAT_FAILED" -ne 0 ]; then
+    ...
+    exit 1
+fi

Scope

The gate itself is unchanged, including which directories it covers, so an unformatted tree still fails the hook. Whether envs/ belongs in that scope is a separate policy question (CI formats only src/ and tests/) and is deliberately not touched here. This PR only changes the failure mode, from mutating or discarding work to reporting the problem.

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation
  • New environment
  • Refactoring

Alignment Checklist

  • I have read .claude/docs/PRINCIPLES.md and this PR aligns with our principles
  • I have checked .claude/docs/INVARIANTS.md and no invariants are violated
  • I have run bash .claude/hooks/lint.sh and tests and addressed all issues

To be precise on the third box: the hook reports pre-existing formatting debt under envs/ on an untouched main as well, which is the policy question noted above. The files in this diff are clean under ruff format --check, ruff check, and usort check, and bash -n passes on the script.

RFC Status

  • Not required (bug fix, docs, minor refactoring)

Test Plan

Adds tests/scripts/test_lint_hook.py, which runs the real hook against a throwaway git repo with a stub uv on PATH. No formatter is actually invoked, so the tests are hermetic and do not depend on network access or on which ruff version CI resolves, while still exercising the real shell logic rather than a reimplementation of it. They complete in about 1.5s.

Four cases, two of which fail on main:

Case Before After
Uncommitted edit survives a run fails, file reverted to HEAD content passes
Clean tree stays clean fails, M envs/demo_env/README.md left modified passes
Still fails when formatting is needed passes passes
Still passes when everything is formatted passes passes

The last two are there so that "stop failing" cannot pass as a fix.

Confirmed against this repo directly. Working tree before and after a run of the real hook:

status BEFORE:  M .claude/hooks/lint.sh   ?? tests/scripts/test_lint_hook.py
hook output:    57 files would be reformatted, 684 files already formatted
                ERROR: the files listed above need formatting.
status AFTER:   M .claude/hooks/lint.sh   ?? tests/scripts/test_lint_hook.py

Identical, and no README churn. On main the same run leaves about 31 READMEs modified.

Full suite: 1534 passed, 133 skipped (baseline 1530 passed, 133 skipped, plus the 4 new cases).

Claude Code Review

/alignment-review: no Tier 1 issues in this diff, no principle conflicts, no RFC conflicts. The hook is contributor tooling and touches neither the agent and orchestration boundary nor client and server separation.


Note

Low Risk
Contributor-only hook and test changes; no runtime product, auth, or data paths affected.

Overview
Fixes a contributor hook that could destroy uncommitted work and dirty the repo. .claude/hooks/lint.sh no longer runs usort format / ruff format and then git checkout -- on changed *.py files. It now uses usort check and ruff format --check with a single FORMAT_FAILED gate, so formatting problems are reported without writing to the tree.

That removes two failure modes from automated flows (alignment review, pre-submit): reverting uncommitted edits when “undoing” formatted files, and leaving reformatted Markdown (e.g. under envs/) dirty because only Python was restored.

Adds tests/scripts/test_lint_hook.py, which runs the real hook in a throwaway git repo with a stub uv to assert uncommitted edits survive, the tree stays clean (including .md), the hook still fails when formatting is needed, and still passes when everything is formatted.

Reviewed by Cursor Bugbot for commit 7acf071. Bugbot is set up for automated code reviews on this repo. Configure here.

…anges

lint.sh formats in place and then runs `git checkout -- $CHANGED` to undo the
formatting. That restores from HEAD, so if a touched file also has uncommitted
edits, those edits are discarded along with the formatting changes.

The hook is run by contributor workflows such as alignment and pre-submit
checks, so this can happen without the author explicitly choosing to format
files. Unformatted files already on main make it easy to hit in practice.

The same step also left the tree dirty: the restore filtered to *.py, but this
ruff version formats Python blocks inside Markdown, so env README files were
rewritten and never restored.

Ask the tools for status instead of writing: `usort check` and
`ruff format --check`. Nothing is modified, so nothing needs restoring, and the
failure mode changes from mutating or discarding work to reporting the problem.
The gate itself is unchanged, including its scope, so an unformatted tree still
fails; whether envs/ belongs in that scope is a separate question left alone
here.

Tests run the real hook against a throwaway git repo with a stub uv on PATH, so
they are hermetic and invoke no formatter. Both failing cases are pinned: an
uncommitted edit must survive a run, and a clean tree must stay clean.
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.

1 participant