Summary
Installing hooks from inside a git worktree fails with ENOTDIR. This is a regression: worktrees were fixed in #88 (which closed #58), but commit 8bb9818 broke them again about two years later.
Affected versions: 2.13.0 and 2.13.1. Version 2.12.x and earlier work correctly.
This looks similar to #58, but the error is different. #58 reported ENOENT on .git/worktrees/<name>/hooks/pre-commit. The current error is ENOTDIR on <worktree>/.git/hooks. So I opened a new issue instead of reopening the old one.
How to reproduce
git init main
cd main
git config user.email t@t.t && git config user.name t
echo '{"name":"x","simple-git-hooks":{"pre-commit":"echo hi"}}' > package.json
git add -A && git commit -m init
git worktree add ../wt -b feat
cd ../wt
npx simple-git-hooks
Result:
ENOTDIR: not a directory, mkdir '.../wt/.git/hooks'
No hook is installed.
Cause
getGitProjectRoot() still handles worktrees correctly. In a worktree, .git is a file, not a directory, so the function reads the gitdir: line and follows commondir back to the main repository. This is the logic that was added in #88, and it still returns the right path today.
The problem is that _setHook() no longer uses this value:
function _setHook(hook, command, projectRoot=process.cwd()) {
const gitRoot = getGitProjectRoot(projectRoot) // correct path
if (!gitRoot) { ... } // but only used as a null check
const hookDirectory = _getHooksDirPath(projectRoot) // projectRoot, not gitRoot
And _getHooksDirPath() builds the default path like this:
const defaultHooksDirPath = path.join(projectRoot, '.git', 'hooks')
In a worktree, <worktree>/.git is a file. Joining hooks onto a file path and calling mkdirSync gives ENOTDIR.
Commit 8bb9818 ("feat: optimize the migration experience from husky", #127) introduced this. It added core.hooksPath support and changed the path computation:
- const hookDirectory = gitRoot + '/hooks/'
+ const hookDirectory = _getHooksDirPath(projectRoot)
The old line used the resolved gitRoot from #88. The new one does not, so the worktree fix became dead code.
I verified this by checking out 8bb9818^ and running the steps above: the install succeeds and the hook is written to main/.git/hooks/pre-commit, which is the correct place.
_removeHook() was changed in the same commit and has the same bug, so uninstall is also broken in a worktree.
One more small hint that this was not intended: the JSDoc of _getHooksDirPath still documents the old contract:
* @param {string} gitRoot - The absolute path to the Git project root
but the parameter is named projectRoot and every caller passes projectRoot.
Suggested fix
Use gitRoot as the base for the default hooks directory again, and keep core.hooksPath support.
One detail worth keeping in mind: a relative core.hooksPath should still be resolved against the worktree working directory, because that is what git itself does. Only the default <gitdir>/hooks path needs to use gitRoot.
Workaround
Set an absolute core.hooksPath before installing:
git config --local core.hooksPath /abs/path/to/hooks
This skips the broken default path.
I will send a PR with the fix and a regression test for worktrees.
Summary
Installing hooks from inside a git worktree fails with
ENOTDIR. This is a regression: worktrees were fixed in #88 (which closed #58), but commit 8bb9818 broke them again about two years later.Affected versions: 2.13.0 and 2.13.1. Version 2.12.x and earlier work correctly.
This looks similar to #58, but the error is different. #58 reported
ENOENTon.git/worktrees/<name>/hooks/pre-commit. The current error isENOTDIRon<worktree>/.git/hooks. So I opened a new issue instead of reopening the old one.How to reproduce
Result:
No hook is installed.
Cause
getGitProjectRoot()still handles worktrees correctly. In a worktree,.gitis a file, not a directory, so the function reads thegitdir:line and followscommondirback to the main repository. This is the logic that was added in #88, and it still returns the right path today.The problem is that
_setHook()no longer uses this value:And
_getHooksDirPath()builds the default path like this:In a worktree,
<worktree>/.gitis a file. Joininghooksonto a file path and callingmkdirSyncgivesENOTDIR.Commit 8bb9818 ("feat: optimize the migration experience from
husky", #127) introduced this. It addedcore.hooksPathsupport and changed the path computation:The old line used the resolved
gitRootfrom #88. The new one does not, so the worktree fix became dead code.I verified this by checking out
8bb9818^and running the steps above: the install succeeds and the hook is written tomain/.git/hooks/pre-commit, which is the correct place._removeHook()was changed in the same commit and has the same bug, so uninstall is also broken in a worktree.One more small hint that this was not intended: the JSDoc of
_getHooksDirPathstill documents the old contract:but the parameter is named
projectRootand every caller passesprojectRoot.Suggested fix
Use
gitRootas the base for the default hooks directory again, and keepcore.hooksPathsupport.One detail worth keeping in mind: a relative
core.hooksPathshould still be resolved against the worktree working directory, because that is what git itself does. Only the default<gitdir>/hookspath needs to usegitRoot.Workaround
Set an absolute
core.hooksPathbefore installing:This skips the broken default path.
I will send a PR with the fix and a regression test for worktrees.