Description
Encountered an error when running the pre-commit hook from a git worktree (a checkout created via git worktree add). The hook attempts to write its run report to ${root}/.git/hooks-cache/pre_commit_report.yml, but in a worktree ${root}/.git is a file containing gitdir: <path>, not a directory, so every mkdir / touch / redirect against that path fails with Not a directory.
The same failure mode applies to git submodules. Anywhere the repository is checked out in a form where ${root}/.git is a gitfile rather than a directory, the hook will fail to write its cache.
The hook does not abort (proceeds when the lint checks), but it emits spurious error lines for every run, and the pre-commit report it is supposed to leave behind is never written. commit-msg then tries to read that report from the same path, so the read side fails silently in the same environments.
The assumption that ${root}/.git is a directory appears in three hooks (pre-commit, pre-push, and commit-msg:
|
# Define the path to a directory for caching hook results: |
|
cache_dir="${root}/.git/hooks-cache" |
|
# Define the path to a directory for caching hook results: |
|
cache_dir="${root}/.git/hooks-cache" |
|
# Define the path to a report generated during the pre-commit hook: |
|
pre_commit_report="${root}/.git/hooks-cache/pre_commit_report.yml" |
All three derive root from git rev-parse --show-toplevel, which correctly returns the working-tree root in every repo layout. But for the path join to .git — that path is only a directory in a regular top-level checkout.
Proposed fix
The portable fix is to ask git directly for the git directory (with git_dir=$(git rev-parse --absolute-git-dir)) instead of path-joining onto ${root}/.git. This would mirror the existing root=$(git rev-parse --show-toplevel) line already present in each hook. So, we could add a sibling git_dir in the prologue, then reference it at the call site:
# Determine root directory:
root=$(git rev-parse --show-toplevel)
# Determine git directory (correct in regular checkouts, worktrees, and submodules):
git_dir=$(git rev-parse --absolute-git-dir)
# At each affected line:
cache_dir="${git_dir}/hooks-cache" # pre-commit:84
cache_dir="${git_dir}/hooks-cache" # pre-push:63
pre_commit_report="${git_dir}/hooks-cache/pre_commit_report.yml" # commit-msg:40
git rev-parse --absolute-git-dir resolves to the correct absolute path in all three repo layouts:
<root>/.git for a regular top-level checkout
<main>/.git/worktrees/<name> for a linked worktree
<superproject>/.git/modules/<name> for a submodule
Plain --git-dir returns .git (relative) in a regular checkout and absolute paths in worktrees/submodules. Relative-vs-absolute today is inconsistent. --absolute-git-dir normalizes to always-absolute, which matches the current ${root}/.git behavior exactly on the happy path — so a reviewer can verify this is a strict superset: same result in a regular checkout, correct result where the old code broke.
This uses the root=$(git rev-parse --show-toplevel) idiom applied to git's other canonical rev-parse query. --show-toplevel gives you the working-tree root (right for resolving source paths); --absolute-git-dir gives you the metadata directory (right for caches and reports).
No side-effect risks were found: (1) no hook changes CWD during execution (no cd/pushd in any of pre-commit, pre-push, commit-msg, post-merge); (2) per-worktree cache is the correct semantic — the cache is transient state for a single in-flight commit, so two concurrent commits in two worktrees need separate caches, not a shared one (--git-common-dir would be wrong here).
Related Issues
No.
Questions
No.
Demo
No response
Reproduction
Worktree case (verified):
git clone https://github.com/stdlib-js/stdlib.git
cd stdlib
# Create a worktree for any branch
git worktree add ../stdlib-wt -b scratch-branch
cd ../stdlib-wt
# Stage any trivial change and commit
echo "// test" >> CONTRIBUTING.md
git add CONTRIBUTING.md
git commit -m "chore: worktree hook repro"
Submodule case (same failure mode — .git is a file):
# In any superproject
git submodule add https://github.com/stdlib-js/stdlib.git vendor/stdlib
cd vendor/stdlib
# Confirm .git is a file, not a directory
test -f .git && echo "gitfile (will trigger bug)"
# Stage any trivial change and commit from within the submodule
echo "// test" >> CONTRIBUTING.md
git add CONTRIBUTING.md
git commit -m "chore: submodule hook repro"
Expected Results
The commit completes without hook-cache write errors, and ${git-dir}/hooks-cache/pre_commit_report.yml is written successfully.
Actual Results
mkdir: <worktree>/.git: Not a directory
touch: <worktree>/.git/hooks-cache/pre_commit_report.yml: Not a directory
tools/git/hooks/pre-commit: line 120: <worktree>/.git/hooks-cache/pre_commit_report.yml: Not a directory
...
(one "Not a directory" line per lint phase)
Version
develop @ d3427df39ee (2026-04-17)
Environments
N/A
Browser Version
No response
Node.js / npm Version
No response
Platform
macOS (arm64); the bug is platform-independent — any worktree hits it.
Checklist
Assisted-by: Claude Opus 4.7 (but I read and understand the whole thing)
Description
Encountered an error when running the
pre-commithook from a git worktree (a checkout created viagit worktree add). The hook attempts to write its run report to${root}/.git/hooks-cache/pre_commit_report.yml, but in a worktree${root}/.gitis a file containinggitdir: <path>, not a directory, so everymkdir/touch/ redirect against that path fails withNot a directory.The same failure mode applies to git submodules. Anywhere the repository is checked out in a form where
${root}/.gitis a gitfile rather than a directory, the hook will fail to write its cache.The hook does not abort (proceeds when the lint checks), but it emits spurious error lines for every run, and the pre-commit report it is supposed to leave behind is never written.
commit-msgthen tries to read that report from the same path, so the read side fails silently in the same environments.The assumption that
${root}/.gitis a directory appears in three hooks (pre-commit,pre-push, andcommit-msg:stdlib/tools/git/hooks/pre-commit
Lines 83 to 84 in d3427df
stdlib/tools/git/hooks/pre-push
Lines 62 to 63 in d3427df
stdlib/tools/git/hooks/commit-msg
Lines 39 to 40 in d3427df
All three derive
rootfromgit rev-parse --show-toplevel, which correctly returns the working-tree root in every repo layout. But for the path join to.git— that path is only a directory in a regular top-level checkout.Proposed fix
The portable fix is to ask git directly for the git directory (with
git_dir=$(git rev-parse --absolute-git-dir)) instead of path-joining onto${root}/.git. This would mirror the existingroot=$(git rev-parse --show-toplevel)line already present in each hook. So, we could add a siblinggit_dirin the prologue, then reference it at the call site:git rev-parse --absolute-git-dirresolves to the correct absolute path in all three repo layouts:<root>/.gitfor a regular top-level checkout<main>/.git/worktrees/<name>for a linked worktree<superproject>/.git/modules/<name>for a submodulePlain
--git-dirreturns.git(relative) in a regular checkout and absolute paths in worktrees/submodules. Relative-vs-absolute today is inconsistent.--absolute-git-dirnormalizes to always-absolute, which matches the current${root}/.gitbehavior exactly on the happy path — so a reviewer can verify this is a strict superset: same result in a regular checkout, correct result where the old code broke.This uses the
root=$(git rev-parse --show-toplevel)idiom applied to git's other canonical rev-parse query.--show-toplevelgives you the working-tree root (right for resolving source paths);--absolute-git-dirgives you the metadata directory (right for caches and reports).No side-effect risks were found: (1) no hook changes CWD during execution (no
cd/pushdin any ofpre-commit,pre-push,commit-msg,post-merge); (2) per-worktree cache is the correct semantic — the cache is transient state for a single in-flight commit, so two concurrent commits in two worktrees need separate caches, not a shared one (--git-common-dirwould be wrong here).Related Issues
No.
Questions
No.
Demo
No response
Reproduction
Worktree case (verified):
Submodule case (same failure mode —
.gitis a file):Expected Results
The commit completes without hook-cache write errors, and
${git-dir}/hooks-cache/pre_commit_report.ymlis written successfully.Actual Results
Version
develop@d3427df39ee(2026-04-17)Environments
N/A
Browser Version
No response
Node.js / npm Version
No response
Platform
macOS (arm64); the bug is platform-independent — any worktree hits it.
Checklist
Assisted-by: Claude Opus 4.7 (but I read and understand the whole thing)