Conversation
When `fail_on_fix = true` on a hook, the hook will fail if fix commands modify files. This is useful with `stage = false` in pre-commit hooks to apply fixes but block the commit, letting users review changes before staging manually. Closes #723 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Summary of ChangesHello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed! This pull request introduces a new Highlights
Changelog
Activity
Using Gemini Code AssistThe full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips. Invoking Gemini You can request assistance from Gemini at any point by creating a comment using either
Customization To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a Limitations & Feedback Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for Github and other Google products, sign up here. You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension. Footnotes
|
There was a problem hiding this comment.
Code Review
This pull request introduces a fail_on_fix option for hooks, a valuable feature for workflows where developers wish to review automatic fixes before committing. The implementation is clean and effective, adding a new field to the configuration and the Hook struct, with the primary logic in the hook's run method. This logic correctly identifies unstaged files after a fix run and fails the hook when fail_on_fix is active. The feature is also well-supported by new bats tests that cover the essential scenarios. I have one minor suggestion to enhance code conciseness.
| .unstaged_files | ||
| .iter() | ||
| .map(|p| format!(" {}", p.display())) | ||
| .collect::<Vec<_>>() |
Greptile SummaryThis PR adds a Key changes:
Issue found: Confidence Score: 3/5
Flowchart%%{init: {'theme': 'neutral'}}%%
flowchart TD
A[hk run hook] --> B{RunType is Fix?}
B -- No --> C[Run step groups in Check mode]
B -- Yes --> D{stash_method not None?}
D -- Yes --> E[Stash pre-existing unstaged changes]
D -- No --> F[Pre-existing unstaged changes remain]
E --> G[Run step groups - fixers execute]
F --> G
G --> H{result ok AND fail_on_fix true?}
H -- No --> I[Skip fail_on_fix check]
H -- Yes --> J[repo.status after fix]
J --> K{unstaged_files empty?}
K -- Yes --> L[result stays Ok]
K -- No --> M[warn and result set to Err\nNOTE: includes pre-existing\nunstaged files if stash is None]
L --> N[pop_stash if applicable]
M --> N
I --> N
C --> O[Return result]
N --> O
Last reviewed commit: 4e0e6b0 |
| if result.is_ok() && self.fail_on_fix && matches!(run_type, RunType::Fix) { | ||
| let status = repo.lock().await.status(None)?; | ||
| if !status.unstaged_files.is_empty() { | ||
| let file_list = status | ||
| .unstaged_files | ||
| .iter() | ||
| .map(|p| format!(" {}", p.display())) | ||
| .collect::<Vec<_>>() | ||
| .join("\n"); | ||
| warn!( | ||
| "Files were fixed but not staged (fail_on_fix=true). Review the changes and stage them manually:\n{}", | ||
| file_list | ||
| ); | ||
| result = Err(eyre::eyre!( | ||
| "fail_on_fix: files were modified by fix commands" | ||
| )); | ||
| } |
There was a problem hiding this comment.
False positive when pre-existing unstaged files exist
The fail_on_fix check reads all unstaged files from the repository after the fixers run, but it does not distinguish between files that were modified by the fixers and files that were already unstaged before the hook ran.
When stage = false (the primary documented use-case), stash_method defaults to StashMethod::None (line 355), so pre-existing unstaged changes are never stashed (line 637-657 skips stashing when method is None). If the user has any unrelated unstaged changes in the working tree, status.unstaged_files will be non-empty even when the fixers made zero modifications — causing the hook to fail when the documented behaviour ("fail when fix commands modify files") says it should pass.
The test "fail_on_fix=true passes when no files are modified" only covers a clean working tree; it does not exercise the case where pre-existing unstaged files coexist with a no-op fixer.
A more accurate implementation would capture the set of unstaged files before the step groups run, then compare against the post-fix set, failing only when there is a meaningful delta:
// Before running groups, snapshot pre-existing unstaged files
let pre_unstaged: BTreeSet<_> = repo.lock().await.status(None)?
.unstaged_files.into_iter().collect();
// ... run groups ...
// fail_on_fix: only fail when fixers actually changed something
if result.is_ok() && self.fail_on_fix && matches!(run_type, RunType::Fix) {
let status = repo.lock().await.status(None)?;
let newly_modified: Vec<_> = status.unstaged_files.iter()
.filter(|p| !pre_unstaged.contains(*p))
.collect();
// Also check files that existed before but now have different content
// (e.g. via git diff --name-only pre_unstaged ∩ post_unstaged)
if !newly_modified.is_empty() {
...
result = Err(eyre::eyre!("fail_on_fix: files were modified by fix commands"));
}
}Without this guard, any user who runs hk with fail_on_fix = true while their worktree contains unrelated unstaged edits will see a spurious failure and a misleading "Files were fixed but not staged" warning even though the fixers touched nothing.
Address review feedback: - Use file content hashing instead of unstaged file set comparison to correctly detect fixer modifications even on already-unstaged files - Avoid early return via `?` that could skip pop_stash cleanup - Add test for pre-existing unstaged files not causing false positives Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Treat deleted files as modified (unwrap_or(true) instead of false) - Skip pre-run file hashing when run_type is Check (not just Fix) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.
- Use neutral warning message that doesn't assume stage=false - Fix no-op test to actually exercise the fail_on_fix code path by creating an unstaged change so hk picks up the file Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
### 🚀 Features - **(hook)** add `fail_on_fix` option by [@jdx](https://github.com/jdx) in [#725](#725) ### 🐛 Bug Fixes - **(builtins)** remove redundant check/check_diff from builtins by [@nkakouros](https://github.com/nkakouros) in [#726](#726) ### 📦️ Dependency Updates - update anthropics/claude-code-action digest to 26ec041 by [@renovate[bot]](https://github.com/renovate[bot]) in [#720](#720) - update jdx/mise-action digest to e79ddf6 by [@renovate[bot]](https://github.com/renovate[bot]) in [#721](#721) - update actions-rust-lang/setup-rust-toolchain digest to a0b538f by [@renovate[bot]](https://github.com/renovate[bot]) in [#719](#719) - update rust crate tokio to v1.50.0 by [@renovate[bot]](https://github.com/renovate[bot]) in [#722](#722) <!-- CURSOR_SUMMARY --> --- > [!NOTE] > **Medium Risk** > Primarily a release/version bump, but it also updates `Cargo.lock` (including TLS/crypto-related crates like `aws-lc-*`), which could introduce build or runtime regressions. > > **Overview** > Prepares the `v1.38.0` release by adding the `1.38.0` section to `CHANGELOG.md` and bumping the project version to `1.38.0` across `Cargo.toml`, CLI usage/docs (`hk.usage.kdl`, `docs/cli/*`), and various docs/examples. > > Updates documentation Pkl `amends`/`import` example URLs to reference the `v1.38.0` release artifacts, and refreshes `Cargo.lock` with updated dependency versions (notably `aws-lc-*`, `windows-sys`, `getrandom`, `socket2`, `tokio-macros`, `uuid`). > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 40b9c73. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> Co-authored-by: mise-en-dev <123107610+mise-en-dev@users.noreply.github.com>
This MR contains the following updates: | Package | Update | Change | |---|---|---| | [hk](https://github.com/jdx/hk) | minor | `1.36.0` → `1.38.0` | MR created with the help of [el-capitano/tools/renovate-bot](https://gitlab.com/el-capitano/tools/renovate-bot). **Proposed changes to behavior should be submitted there as MRs.** --- ### Release Notes <details> <summary>jdx/hk (hk)</summary> ### [`v1.38.0`](https://github.com/jdx/hk/blob/HEAD/CHANGELOG.md#1380---2026-03-06) [Compare Source](jdx/hk@v1.37.0...v1.38.0) ##### 🚀 Features - **(hook)** add `fail_on_fix` option by [@​jdx](https://github.com/jdx) in [#​725](jdx/hk#725) ##### 🐛 Bug Fixes - **(builtins)** remove redundant check/check\_diff from builtins by [@​nkakouros](https://github.com/nkakouros) in [#​726](jdx/hk#726) ##### 📦️ Dependency Updates - update anthropics/claude-code-action digest to [`26ec041`](jdx/hk@26ec041) by [@​renovate\[bot\]](https://github.com/renovate\[bot]) in [#​720](jdx/hk#720) - update jdx/mise-action digest to [`e79ddf6`](jdx/hk@e79ddf6) by [@​renovate\[bot\]](https://github.com/renovate\[bot]) in [#​721](jdx/hk#721) - update actions-rust-lang/setup-rust-toolchain digest to [`a0b538f`](jdx/hk@a0b538f) by [@​renovate\[bot\]](https://github.com/renovate\[bot]) in [#​719](jdx/hk#719) - update rust crate tokio to v1.50.0 by [@​renovate\[bot\]](https://github.com/renovate\[bot]) in [#​722](jdx/hk#722) ### [`v1.37.0`](https://github.com/jdx/hk/blob/HEAD/CHANGELOG.md#1370---2026-03-03) [Compare Source](jdx/hk@v1.36.0...v1.37.0) ##### 🚀 Features - **(hook)** add env support to hooks by [@​jdx](https://github.com/jdx) in [#​709](jdx/hk#709) - parse Go-style diffs by [@​jdx](https://github.com/jdx) in [#​704](jdx/hk#704) ##### 🐛 Bug Fixes - **(builtins)** strip extra trailing newlines in end-of-file-fixer by [@​jdx](https://github.com/jdx) in [#​708](jdx/hk#708) - **(docs)** correctly document what --all is about by [@​nkakouros](https://github.com/nkakouros) in [#​715](jdx/hk#715) - **(git)** exclude untracked files from unstaged\_files set by [@​nkakouros](https://github.com/nkakouros) in [#​716](jdx/hk#716) - **(hkrc)** config format and load order by [@​ivy](https://github.com/ivy) in [#​710](jdx/hk#710) - **(release)** write release notes to file instead of capturing stdout by [@​jdx](https://github.com/jdx) in [#​688](jdx/hk#688) - **(release)** make release notes editorialization non-blocking by [@​jdx](https://github.com/jdx) in [#​690](jdx/hk#690) - **(step)** gate check\_diff forced check\_first on Fix mode only by [@​nkakouros](https://github.com/nkakouros) in [#​717](jdx/hk#717) ##### 📚 Documentation - **(shanty)** add audio player with sea shanty recording by [@​jdx](https://github.com/jdx) in [67a25ad](jdx/hk@67a25ad) - document config file search paths by [@​ivy](https://github.com/ivy) in [#​701](jdx/hk#701) - require AI disclosure on GitHub comments by [@​jdx](https://github.com/jdx) in [#​703](jdx/hk#703) ##### 🔍 Other Changes - replace gen-release-notes script with communique by [@​jdx](https://github.com/jdx) in [#​700](jdx/hk#700) - add autofix.ci workflow by [@​jdx](https://github.com/jdx) in [#​705](jdx/hk#705) ##### 📦️ Dependency Updates - lock file maintenance by [@​renovate\[bot\]](https://github.com/renovate\[bot]) in [#​686](jdx/hk#686) - update taiki-e/upload-rust-binary-action digest to [`f391289`](jdx/hk@f391289) by [@​renovate\[bot\]](https://github.com/renovate\[bot]) in [#​692](jdx/hk#692) - update anthropics/claude-code-action digest to [`c22f7c3`](jdx/hk@c22f7c3) by [@​renovate\[bot\]](https://github.com/renovate\[bot]) in [#​691](jdx/hk#691) - update rust crate libc to v0.2.181 by [@​renovate\[bot\]](https://github.com/renovate\[bot]) in [#​694](jdx/hk#694) - update rust crate clap to v4.5.58 by [@​renovate\[bot\]](https://github.com/renovate\[bot]) in [#​693](jdx/hk#693) - lock file maintenance by [@​renovate\[bot\]](https://github.com/renovate\[bot]) in [#​695](jdx/hk#695) - update anthropics/claude-code-action digest to [`edd85d6`](jdx/hk@edd85d6) by [@​renovate\[bot\]](https://github.com/renovate\[bot]) in [#​698](jdx/hk#698) - update rust crate clap to v4.5.60 by [@​renovate\[bot\]](https://github.com/renovate\[bot]) in [#​699](jdx/hk#699) - lock file maintenance by [@​renovate\[bot\]](https://github.com/renovate\[bot]) in [#​702](jdx/hk#702) - lock file maintenance by [@​renovate\[bot\]](https://github.com/renovate\[bot]) in [#​711](jdx/hk#711) ##### New Contributors - [@​ivy](https://github.com/ivy) made their first contribution in [#​710](jdx/hk#710) - [@​nkakouros](https://github.com/nkakouros) made their first contribution in [#​715](jdx/hk#715) </details> --- ### Configuration 📅 **Schedule**: Branch creation - At any time (no schedule defined), Automerge - At any time (no schedule defined). 🚦 **Automerge**: Disabled by config. Please merge this manually once you are satisfied. ♻ **Rebasing**: Whenever MR becomes conflicted, or you tick the rebase/retry checkbox. 🔕 **Ignore**: Close this MR and you won't be reminded about this update again. --- - [ ] <!-- rebase-check -->If you want to rebase/retry this MR, check this box --- This MR has been generated by [Renovate Bot](https://github.com/renovatebot/renovate). <!--renovate-debug:eyJjcmVhdGVkSW5WZXIiOiI0My40OS4wIiwidXBkYXRlZEluVmVyIjoiNDMuNTcuMCIsInRhcmdldEJyYW5jaCI6Im1haW4iLCJsYWJlbHMiOlsiUmVub3ZhdGUgQm90IiwiYXV0b21hdGlvbjpib3QtYXV0aG9yZWQiLCJkZXBlbmRlbmN5LXR5cGU6Om1pbm9yIl19-->
Summary
fail_on_fixboolean option to hooks that causes the hook to fail when fix commands modify filesstage = falsein pre-commit hooks: fixes are applied but the commit is blocked so users can review changes before staging manuallyfalseto preserve existing behaviorCloses #723
Test plan
fail_on_fix=truefails on modified files, passes when nothing changes, default behavior unchangedstage_settingandstage_defaulttests still pass🤖 Generated with Claude Code
Note
Medium Risk
Adds new hook execution behavior that can force
fixruns to error based on file content changes, which may impact workflows when enabled and introduces extra file I/O/hashing during fix runs.Overview
Adds a new hook config flag,
fail_on_fix(defaultfalse), to makefixhooks fail if any files were actually modified by fix commands—useful forstage = falseworkflows where users want fixes applied but commits blocked until manual review.Implements this by snapshotting pre-run file content hashes for the hook’s file set and comparing after step groups complete in
RunType::Fix, then returning an error and logging the modified file list when changes are detected. Adds Bats coverage for failing/passing cases and for ignoring pre-existing unrelated unstaged changes.Written by Cursor Bugbot for commit c4803c4. This will update automatically on new commits. Configure here.