Summary
_getHooksDirPath() reads core.hooksPath with git config --local. Git also
has a worktree config scope (.git/worktrees/<name>/config.worktree, gated
by extensions.worktreeConfig), and --local cannot see it.
When core.hooksPath is set at worktree scope, the install reports success but
writes the hook to the default directory, while git reads hooks from the
configured one. The hook silently never runs.
This is currently masked by #148. On 2.13.1 the repro below dies with
ENOTDIR before reaching this code. #149 fixes the ENOTDIR and keeps
git config --local, so once it lands this becomes the observable behaviour.
Filing it as a follow-up rather than a comment on #149, since it needs a
separate change.
Tested against #149's approach (default path derived from gitRoot), git 2.50.1.
How to reproduce
Requires #149 applied. On unmodified 2.13.1 you get #148's ENOTDIR instead.
git init main && cd main
git config user.email t@t.t && git config user.name t
echo x > f && git add -A && git commit -m init
git worktree add ../wt -b feat
cd ../wt
# hooks live somewhere custom, configured at worktree scope
mkdir -p ../customhooks
git config extensions.worktreeConfig true
git config --worktree core.hooksPath "$(cd ../customhooks && pwd)"
# a hook that must block the commit
echo '{"name":"t","simple-git-hooks":{"pre-commit":"echo HOOK_FIRED; exit 1"}}' > package.json
npx simple-git-hooks
git config user.email t@t.t && git config user.name t
echo y > g && git add g && git commit -m "should be blocked"
With #149 applied:
[INFO] Successfully set the pre-commit with command: echo HOOK_FIRED; exit 1
[feat 829bc19] should be blocked
1 file changed, 1 insertion(+)
The commit is not blocked and HOOK_FIRED never prints. The hook went to
main/.git/hooks/pre-commit; git reads from customhooks/, which stays empty.
Nothing reports a problem.
On unmodified 2.13.1 the same steps give:
ENOTDIR: not a directory, mkdir '.../wt/.git/hooks'
which is #148.
Cause
Git resolves core.hooksPath from the effective config, and worktree scope
outranks local. From inside the worktree above:
$ git config --get core.hooksPath # effective, what git uses
/tmp/x/customhooks
$ git config --worktree --get core.hooksPath
/tmp/x/customhooks
$ git config --local --get core.hooksPath # what simple-git-hooks reads
# (empty)
$ git config --show-origin --get core.hooksPath
file:/tmp/x/main/.git/worktrees/wt/config.worktree /tmp/x/customhooks
_getHooksDirPath() sees an empty string from --local, reads that as
"not configured", and returns the default path.
Two fixes that look right but are not
Dropping --local. This reintroduces #130: a global core.hooksPath
would redirect a project's hooks into a shared directory and clobber hooks for
every other project on the machine. I confirmed the regression by setting a
global core.hooksPath through GIT_CONFIG_GLOBAL and watching the hook land
there instead of in the repo. --local needs to stay.
git rev-parse --git-path hooks. Tempting, and it returns the right answer
for both worktree scope and the #148 default-path case. But it resolves the
effective config, so it honours a global core.hooksPath too and hits the
same #130 problem. It can also return a relative path (.git/hooks when run
from the repo root), which would need resolving.
Suggested fix
Consult the two repo-level scopes in precedence order, never global or system:
function _getRepoScopedHooksPath(projectRoot) {
const opts = { cwd: projectRoot, encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }
for (const scope of ['--worktree', '--local']) {
try {
const value = execSync(`git config ${scope} --get core.hooksPath`, opts).trim()
if (value) return value
} catch {
// --worktree fails unless extensions.worktreeConfig is enabled, and
// `--get` exits 1 when the key is unset; both mean "try the next scope"
}
}
return ''
}
then use it in place of the current execSync call, keeping the existing
absolute/relative handling and #149's gitRoot-based default.
I checked this against five cases: nothing set, worktree with the extension
off, global-only (correctly ignored, so #130 stays fixed), local-only, and
worktree overriding local. The last matches git config --get core.hooksPath.
One note on the --worktree branch: it exits non-zero with
fatal: --worktree cannot be used with multiple working trees unless the config
extension worktreeConfig is enabled.
when the extension is off, which is the common case, so it has to be caught
rather than treated as an error.
Summary
_getHooksDirPath()readscore.hooksPathwithgit config --local. Git alsohas a worktree config scope (
.git/worktrees/<name>/config.worktree, gatedby
extensions.worktreeConfig), and--localcannot see it.When
core.hooksPathis set at worktree scope, the install reports success butwrites the hook to the default directory, while git reads hooks from the
configured one. The hook silently never runs.
This is currently masked by #148. On 2.13.1 the repro below dies with
ENOTDIRbefore reaching this code. #149 fixes theENOTDIRand keepsgit config --local, so once it lands this becomes the observable behaviour.Filing it as a follow-up rather than a comment on #149, since it needs a
separate change.
Tested against #149's approach (default path derived from
gitRoot), git 2.50.1.How to reproduce
Requires #149 applied. On unmodified 2.13.1 you get #148's
ENOTDIRinstead.With #149 applied:
The commit is not blocked and
HOOK_FIREDnever prints. The hook went tomain/.git/hooks/pre-commit; git reads fromcustomhooks/, which stays empty.Nothing reports a problem.
On unmodified 2.13.1 the same steps give:
which is #148.
Cause
Git resolves
core.hooksPathfrom the effective config, and worktree scopeoutranks local. From inside the worktree above:
_getHooksDirPath()sees an empty string from--local, reads that as"not configured", and returns the default path.
Two fixes that look right but are not
Dropping
--local. This reintroduces #130: a globalcore.hooksPathwould redirect a project's hooks into a shared directory and clobber hooks for
every other project on the machine. I confirmed the regression by setting a
global
core.hooksPaththroughGIT_CONFIG_GLOBALand watching the hook landthere instead of in the repo.
--localneeds to stay.git rev-parse --git-path hooks. Tempting, and it returns the right answerfor both worktree scope and the #148 default-path case. But it resolves the
effective config, so it honours a global
core.hooksPathtoo and hits thesame #130 problem. It can also return a relative path (
.git/hookswhen runfrom the repo root), which would need resolving.
Suggested fix
Consult the two repo-level scopes in precedence order, never global or system:
then use it in place of the current
execSynccall, keeping the existingabsolute/relative handling and #149's
gitRoot-based default.I checked this against five cases: nothing set, worktree with the extension
off, global-only (correctly ignored, so #130 stays fixed), local-only, and
worktree overriding local. The last matches
git config --get core.hooksPath.One note on the
--worktreebranch: it exits non-zero withwhen the extension is off, which is the common case, so it has to be caught
rather than treated as an error.