threads / discuss / 66759

Interaction between pre-commit hook and git commit <pathspec>

Subject: Interaction between pre-commit hook and git commit <pathspec>

## tl;dr

A user reports that index changes made by a pre-commit hook vanish when committing with a pathspec; Junio Hamano suggests the answer is clearer documentation, maybe an error. Read the story.

replies: 4people: 4as markdown or json

Kevin Lyles· Nov 3, 2026, 19:58 UTC · lore
Hello, folks,

The documentation for the pathspec option of the commit command claims: "The contents of these files are also staged for the next commit on top of what have been staged before." However, that appears to only be true if the pre-commit hook doesn't modify the index contents.

Here's an example pre-commit script:
    #!/bin/sh
    
    git diff --staged --name-only | while read -r file
    do
      {
        git cat-file blob :"$file"
        echo '# Extra text'
      } |
        {
          echo -n "100644 blob "
          echo -n $(git hash-object -w -t blob --path "$file" --stdin)
          echo -e "\t$file"
        } |
          git update-index --index-info
      echo '# Extra text' >> "$file"
    done

This always appends "# Extra text" to the end of any staged files, in both the index and the worktree. It serves to illustrate the issue, without getting into the weeds of what my pre-commit hook is doing. Suffice to say that my hook is (sometimes) modifying files in both the index (to make sure the updated contents are committed) and the worktree (to reduce spurious diffs when file contents are changed).

Here's a set of steps to reproduce the issue:
1. Create a new repository with `git init'
2. Set up the pre-commit script above
3. Create a file and commit it
4. Modify that file
5. Run `git commit <file path>'
6. See that the worktree includes the pre-commit changes, but the index
   is missing them.  Alternatively, see that running `git add <file>'
   results in that file having no changes.

If any other files are already staged between steps 3 and 5, those are included in the commit and the index includes the pre-commit changes. Additionally, adding the file and then committing without specifying a file path results in the expected behavior: both the index and worktree end up with the pre-commit changes.

Are these differences in behavior intentional?

It's possible that there is a better way to achieve my goal than a pre-commit hook. I'm open to suggestions. However, anything that would run per-file instead of per-commit will likely be too slow to use in our (~4x the Linux kernel repository by file count) repository, at least for some large commits.

Thank you, Kevin

Kevin Lyles (he/him) Epic R&D - Hyperspace klyles@epic.com

Kristoffer Haugsbakk· Nov 3, 2026, 20:13 UTC · re: Kevin Lyles · lore

Re: Interaction between pre-commit hook and git commit <pathspec>

On Tue, Nov 3, 2026, at 20:58, Kevin Lyles wrote:
Show 33 quoted lines
> Hello, folks,
>
> The documentation for the pathspec option of the commit command claims:
> "The contents of these files are also staged for the next commit on top
> of what have been staged before."
> However, that appears to only be true if the pre-commit hook doesn't
> modify the index contents.
>
> Here's an example pre-commit script:
>     #!/bin/sh
>    
>     git diff --staged --name-only | while read -r file
>     do
>       {
>         git cat-file blob :"$file"
>         echo '# Extra text'
>       } |
>         {
>           echo -n "100644 blob "
>           echo -n $(git hash-object -w -t blob --path "$file" --stdin)
>           echo -e "\t$file"
>         } |
>           git update-index --index-info
>       echo '# Extra text' >> "$file"
>     done
>
> This always appends "# Extra text" to the end of any staged files, in
> both the index and the worktree. It serves to illustrate the issue,
> without getting into the weeds of what my pre-commit hook is doing.
> Suffice to say that my hook is (sometimes) modifying files in both the
> index (to make sure the updated contents are committed) and the
> worktree (to reduce spurious diffs when file contents are changed).
>[snip]

Reading githooks documentation for pre-commit. To me it reads like the use case is validation/checking, not any mutation. This doesn't sound like a reasonable use case.

Junio C Hamano· Nov 3, 2026, 20:38 UTC · re: Kristoffer Haugsbakk · lore

Re: Interaction between pre-commit hook and git commit <pathspec>

"Kristoffer Haugsbakk" <kristofferhaugsbakk@fastmail.com> writes:
> Reading githooks documentation for pre-commit. To me it reads like
> the use case is validation/checking, not any mutation.

Yes, the hook was written exactly for validation, to preempt an undesirable commit from being created.

We might want to do two things:
(1) clarify the documentation to say that the hook should not try
    mutation, and
(2) detect and error out when the hook does try to make any
    mutation.
I am not sure if (2) is feasible, though.
Thanks.
Chris Torek· Nov 3, 2026, 20:43 UTC · re: Kevin Lyles · lore

Re: Interaction between pre-commit hook and git commit <pathspec>

On Tue, Nov 3, 2026 at 12:00 PM Kevin Lyles <klyles@epic.com> wrote:
Show 5 quoted lines
> The documentation for the pathspec option of the commit command claims:
> "The contents of these files are also staged for the next commit on top
> of what have been staged before."
> However, that appears to only be true if the pre-commit hook doesn't
> modify the index contents.
There's an implementation detail here that makes this tricky.

Git documentation talks about "the" index / staging-area. Or, more precisely, there is one such entity per work-tree: the work-tree's index.

But when you use "git commit --only" or "git commit --include", well, now there is a big problem to deal with. If the commit is accepted, the ("the") index / staging-area should be updated accordingly. But if it's aborted, the index should roll back to how it was before you ran "git commit [--only | --include] <pathspecs>".

In order to make this work internally, Git actually needs two or even three index-es (indices?) / staging-areas for the work-tree in question: the one for "before the commit", possibly a separate one for "making the commit" (when using --only), and one for "after the commit succeeds, assuming it does succeed".

To get a little bit more concrete (which makes describing the underlying process a lot easier), let's note now that "the index" is really a simple file. The main one is ".git/index". (I'm ignoring extra complications like a split index here as they don't affect the result, so this is purely an internal implementation detail, but if you ever dive into the Git code, it's something else to trip over....)

Git handles the "old version, temporary intermediate version if needed, new version" problem by creating two or three files total. One of these is named ".git/index.lock", which serves as both a locking file to prevent two "git commit" operations from running at the same time and stepping on each other, and also "the future version of .git/index, should the commit succeed".

(The third file, which doesn't apply to your specific case, has a funky temporary name with a unique set of characters sprinkled in.)

If your pre-commit hook attempts modification(s) of the proposed snapshot for the new commit, it may need to update more than one of these files to make all three conditions hold, and in general it just can't. (It's too hard to know what the correct names are, since they're different in a work-tree and the "--only" name is randomized.) So pre-commit hooks are restricted to "ok" or "not ok" actions.

> It's possible that there is a better way to achieve my goal than a
> pre-commit hook.

Instead of running "git commit" directly, why not run a script that does the appropriate "git add"s, then runs "git commit" for you? The pre-commit hook can then inspect to be sure that this has occurred, and if not, remind users to run the script instead.

Chris
Kevin Lyles· Nov 4, 2026, 16:34 UTC · re: Chris Torek · lore

RE: Interaction between pre-commit hook and git commit <pathspec>

On Tue, November 3, 2026 14:43 Chris Torek <chris.torek@gmail.com> wrote:

Show 7 quoted lines
> If your pre-commit hook attempts modification(s) of the proposed
> snapshot for the new commit, it may need to update more than
> one of these files to make all three conditions hold, and in general
> it just can't. (It's too hard to know what the correct names are,
> since they're different in a work-tree and the "--only" name is
> randomized.)  So pre-commit hooks are restricted to "ok" or
> "not ok" actions.

It sounds like this is one of the "not ok" actions. I did look at updating the "primary" index as well in this case, but ran into issues with the current commit needing to be done before I can get that lock.

Show 5 quoted lines
> Instead of running "git commit" directly, why not run a script
> that does the appropriate "git add"s, then runs "git commit"
> for you?  The pre-commit hook can then inspect to be sure
> that this has occurred, and if not, remind users to run the
> script instead.

This will be rolled out across my company once we figure out how to implement it. The goal is to avoid changing how people interact with git, while still making these changes automatically. In particular, we'd like it to still work when a file is partially staged.

We could pretty easily validate that the files are in the correct state in the pre-commit hook, but we're trying to avoid an extra step for each developer in normal workflows.

I've also looked at implementing this as a clean filter, using a long-running process for performance reasons. However, also for performance reasons, we need to know the full list of files to be cleaned at the start. Otherwise, some commits will take significantly longer than currently, as we need to know the presence/absence of files near the cleaned files to correctly update the file contents. We can batch finding those files, but only if we either know the full list of files being cleaned ahead of time or always get the full list of files in the repository. `git ls-files' takes anywhere from ~500 ms (hot cache) to ~5 seconds in our repository when listing everything, so we'd prefer to avoid that performance hit when possible.

← back to recent threads