How can I recover the commit message when the git commit-msg hook fails?

The commit message is stored in .git/COMMIT_EDITMSG. After a "failed" committing attempt, you could run:

git commit --edit --file=.git/COMMIT_EDITMSG

or shorter, e.g.:

git commit -eF .git/COMMIT_EDITMSG

which will load the bad commit message in your $EDITOR (or the editor you set up in your Git configuration), so that you can try to fix the commit message. You could also set up an alias for the above, with:

git config --global alias.fix-commit 'commit --edit --file=.git/COMMIT_EDITMSG'

and then use git fix-commit instead.


Background

As stated, when running git commit, git starts your editor pointing to the $GIT_DIR/COMMIT_EDITMSG file. Unless the commit-msg hook in question moves/deletes/damages the file, the message should still be there.

I suppose that reusing the message is not the default behavior because it might interfere with the prepare-commit-msg hook. Ideally, there would be a toggle available to enable reusing by default, in order to avoid data loss. The next-best thing would be to override a git sub-command with a git alias, but unfortunately it is currently not possible and that is unlikely to change. So we are left with creating a custom alias for it. I went with an alias similar to the one in the accepted answer:

git config alias.recommit \
'!git commit -F "$(git rev-parse --git-dir)/COMMIT_EDITMSG" --edit'

Then, when running git recommit, the rejected commit message's content should appear in the editor.

Addition

Note that both aliases would fail for the first commit in the repository, since the COMMIT_EDITMSG file would not have been created yet. To make it also work in that case, it looks a bit more convoluted:

git config alias.recommit \
'!test -f "$(git rev-parse --git-dir)/COMMIT_EDITMSG" &&
git commit -F "$(git rev-parse --git-dir)/COMMIT_EDITMSG" --edit ||
git commit'

Which can be shortened to:

git config alias.recommit \
'!cm="$(git rev-parse --git-dir)/COMMIT_EDITMSG" &&
test -f "$cm" && git commit -F "$cm" --edit || git commit'

Either way, considering the added safety, for interactive usage you could even use one of the aforementioned aliases by default instead of git commit.

You could also make a wrapper for git itself and divert the calls based on the arguments (i.e.: on the sub-command), though that would require ensuring that all subsequent calls to git refer to the original binary, lest they result in infinite recursion:

git () {
    cm="$(git rev-parse --git-dir)/COMMIT_EDITMSG"

    case "$1" in
    commit)
        shift
        test -f "$cm" && command git commit -F "$cm" --edit "$@" ||
        command git commit "$@"
        ;;
    *)
        command git "$@";;
    esac
}

Note that if the above is added to your rc file (e.g.: ~/.bashrc), then every call to git present in it will refer to the wrapper, unless you prepend them with command as well.

Novelty

Finally, I just learned that aliasing to a wrapper file with a different name is an option:

PATH="$HOME/bin:$PATH"
export PATH
alias git='my-git'

So the wrapper (e.g.: ~/bin/my-git) can be much simpler:

#!/bin/sh
cm="$(git rev-parse --git-dir)/COMMIT_EDITMSG"

case "$1" in
commit)
    shift
    test -f "$cm" && git commit -F "$cm" --edit "$@" ||
    git commit "$@"
    ;;
*)
    git "$@";;
esac

And also avoid interference, as aliases are not expanded when used in external scripts.