You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
In a linked git worktree — one created by git worktree add — .git is a file containing gitdir: <path>, not a directory. So >> .git/neotoma-hook-timings.log fails with:
sh: .git/neotoma-hook-timings.log: Not a directory
Impact, stated accurately
This does not block commits. The hook's own comment says a logging failure must never fail a commit, and || true is present on hook_log. That guard works.
What it does is emit that error to stderr on every commit made from any linked worktree. Two agents working the same PR from two different worktrees hit it independently on 2026-09-18, which is how it surfaced — not from one report but from two, in worktrees created for unrelated reasons.
The cost is noise in exactly the place noise is most expensive: a commit's stderr, where a real failure has to compete for attention with a recurring benign one. That is the mechanism by which a genuine hook error gets scrolled past.
Fix
Resolve the real common git directory at hook start rather than hardcoding .git/:
git rev-parse --git-common-dir
That returns the shared .git directory correctly from a linked worktree as well as from the main clone, so the log path is always a real directory.
Acceptance criteria
A commit from a linked worktree produces no Not a directory error
A commit from the main clone still appends timing rows to the same shared log
Verified from BOTH a linked worktree and the main clone, since a fix that works in one and not the other is the current state with the cases swapped
The || true guard stays — the fix removes the error, it does not become the reason a logging failure could fail a commit
Found 2026-09-18 while working neotoma PR #2357 from neotoma-wt-2357fix and neotoma-wt-g25-reltypes.
Swarm specification
This section is maintained by the Ateles swarm. Each lens agent owns exactly one subsection below; the human-written description above these markers is never modified.
Design basis
Design basis: docs/foundation/principles.md#6-extend-the-mechanism-that-already-generalizes-do-not-build-a-parallel-one — the worktree timing-log defect already has a signed path on #2437; this issue scopes that effect and does not add a second log.
Product / Scope (PM)
Problem. A commit from a linked worktree prints Not a directory because .husky/pre-commit appends timings through .git/neotoma-hook-timings.log, and in a linked worktree .git is a file. Still true on origin/main (2026-09-21).
In scope
The existing timing-log write only: it must hit a real directory, and a logging failure must not fail the commit.
One effect test, run from a linked worktree and from the main clone.
The test goes red if the default path is restored to .git/neotoma-hook-timings.log (the missing row / the stderr line, not merely that a git command was invoked).
Single surface: husky pre-commit. No MCP, HTTP, or CLI twin.
Priority and sequencing. Confidence: high. Assumption: #2437's per-worktree decision stands. Do not schedule a third implementation. Arch confirms the #2437 ADR rather than choosing a new path.
Open questions. None that block sequencing.
Engineering
Files to touch
.husky/pre-commit — only the line(s) computing NEOTOMA_HOOK_LOG and the hook_log write.
Concrete change
Replace the hardcoded .git/ path with a per-worktree-safe resolution, consistent with the #2437 ADR (per-worktree logs, not --git-common-dir aggregation):
Use git rev-parse --git-dir (NOT --git-common-dir) — --git-dir resolves to the per-worktree private dir (e.g. .git/worktrees/<name>) in a linked worktree and to .git in the main clone, giving each worktree its own log file, matching PM's "per-worktree, matching the #2437 decision" acceptance criterion. --git-common-dir would collapse all worktrees onto one shared file, which arch already rejected.
Keep || true (or equivalent) on every write through hook_log exactly as-is — no change to the non-blocking guard.
Data/contract changes
None. No schema, MCP, CLI, HTTP, or entity changes. Pure shell path resolution inside a git hook.
Layering
Single surface: .husky/pre-commit only. Do not touch any other hook file, gate stage, or shared script.
Apply the git rev-parse --git-dir resolution above, preserving the ${NEOTOMA_HOOK_LOG:-...} override pattern so callers can still set the env var explicitly.
Add an effect test (shell-level, e.g. under the repo's existing hook/script test harness or a small standalone script invoked from CI) that:
Creates a linked worktree via git worktree add.
Runs the pre-commit hook (or the log-path-resolution logic in isolation) from that worktree.
Asserts stderr contains no Not a directory.
Asserts exit status is unaffected by the log write.
Asserts a timing row is appended under that worktree's resolved git-dir.
Repeats the same run from the main clone and asserts a row lands in the main clone's .git/, in a distinct file from the worktree's.
Asserts the test fails if the path is reverted to the literal .git/neotoma-hook-timings.log default (i.e. the assertion checks the row/stderr, not merely that a git command executed).
Run the new test locally from both a linked worktree and the main clone per the acceptance criteria; confirm both pass.
Regenerate any generated/derived files only if this touches source they depend on (none expected — pure hook script change, no test-catalog entry needed unless the new test file is added under a catalog-tracked test directory, in which case run npm run generate:test-catalog).
Open PR referencing closes #2468, with design basis line: Design basis: docs/foundation/principles.md#6-extend-the-mechanism-that-already-generalizes-do-not-build-a-parallel-one, and note in the PR body that this lands the Pre-commit hook blocks every commit from a git worktree: timing log hard-codes .git/ as a directory #2437-approved per-worktree resolution for this specific call site rather than introducing a new pattern.
QA / Test Plan
Prior art.#2333, #2437, and this issue are the same .git-as-file write. No implementing PR. On origin/main, .husky/pre-commit line 7 is still NEOTOMA_HOOK_LOG="${NEOTOMA_HOOK_LOG:-.git/neotoma-hook-timings.log}". #2437 already names the eval file below. Do not add a second test.
Surface. Husky pre-commit instrumentation only (NEOTOMA_HOOK_LOG and hook_log in .husky/pre-commit). Not MCP, HTTP, CLI, or schema. No agentic_eval fixture and no eval:tier1. The vitest file is the gate; its CI result is the QA report.
Eval.tests/scripts/pre_commit_hook_timings_worktree.test.ts (absent). Command: npx vitest run tests/scripts/pre_commit_hook_timings_worktree.test.ts. Same PR regenerates docs/testing/automated_test_catalog.md (tests/scripts/ is catalogued).
Harness. Temp repo via real git init and git worktree add — do not mock .git. spawnSync("sh", ["-e", slice], { cwd }) on the production block from # --- Hook instrumentation through the closing # ---------------------------------------------------------------------------, then one hook_log call. Do not run type-check, lint, or the hook's npm test. Do not reimplement the path logic in the test.
Definition of done
Worktree stderr (this bug). Linked worktree (.git is a file), NEOTOMA_HOOK_LOG unset. Process stderr does not contain Not a directory. The 2>/dev/null on printf does not hide the shell's failed >> open; assert the child stderr.
Worktree row. That run appends a line containing stage or total at git -C <worktree> rev-parse --git-path neotoma-hook-timings.log. The path is not <worktree>/.git/neotoma-hook-timings.log.
Main clone, different file. Primary checkout, unset override: a row at that checkout's --git-path, and that path is not the worktree path. This fails a --git-common-dir default (one shared file).
Exit 0 is not acceptance. Both runs exit 0 under sh -e. On current origin/main, hook_log's || true already returns 0 while stderr still prints Not a directory and the worktree file is never created. A test that only checks exit status stays green on this bug.
Decoration proof. With the default restored to .git/neotoma-hook-timings.log, the worktree case fails because stderr contains Not a directory and/or the --git-path file gained no row — not because a git command was or was not invoked. Do not require the source to say --git-dir rather than --git-path. Either is acceptable when the two files differ. --git-common-dir is not.
Override.NEOTOMA_HOOK_LOG set to a temp file: the row lands there and not in the git-dir log. The ${NEOTOMA_HOOK_LOG:-...} form stays.
Unwritable log.NEOTOMA_HOOK_LOG pointed at a directory: sh -e exits 0. || true on hook_log stays. Type-check, lint, and test stages are unchanged.
No new endpoint contracts. Nothing to test on MCP, HTTP, or CLI.
Run result. Not run. The file is not in the tree, so there is no CI link. gate_status.qa stays pending until this test is committed and green.
.husky/pre-commitwrites its timing log to.git/, which is a FILE in a linked worktree.husky/pre-commitsets:NEOTOMA_HOOK_LOG="${NEOTOMA_HOOK_LOG:-.git/neotoma-hook-timings.log}"In a linked git worktree — one created by
git worktree add—.gitis a file containinggitdir: <path>, not a directory. So>> .git/neotoma-hook-timings.logfails with:Impact, stated accurately
This does not block commits. The hook's own comment says a logging failure must never fail a commit, and
|| trueis present onhook_log. That guard works.What it does is emit that error to stderr on every commit made from any linked worktree. Two agents working the same PR from two different worktrees hit it independently on 2026-09-18, which is how it surfaced — not from one report but from two, in worktrees created for unrelated reasons.
The cost is noise in exactly the place noise is most expensive: a commit's stderr, where a real failure has to compete for attention with a recurring benign one. That is the mechanism by which a genuine hook error gets scrolled past.
Fix
Resolve the real common git directory at hook start rather than hardcoding
.git/:That returns the shared
.gitdirectory correctly from a linked worktree as well as from the main clone, so the log path is always a real directory.Acceptance criteria
Not a directoryerror|| trueguard stays — the fix removes the error, it does not become the reason a logging failure could fail a commitFound 2026-09-18 while working neotoma PR #2357 from
neotoma-wt-2357fixandneotoma-wt-g25-reltypes.Swarm specification
This section is maintained by the Ateles swarm. Each lens agent owns exactly one subsection below; the human-written description above these markers is never modified.
Design basis
Design basis: docs/foundation/principles.md#6-extend-the-mechanism-that-already-generalizes-do-not-build-a-parallel-one — the worktree timing-log defect already has a signed path on #2437; this issue scopes that effect and does not add a second log.
Product / Scope (PM)
Problem. A commit from a linked worktree prints
Not a directorybecause.husky/pre-commitappends timings through.git/neotoma-hook-timings.log, and in a linked worktree.gitis a file. Still true onorigin/main(2026-09-21).In scope
Out of scope
--git-common-dir). Pre-commit hook blocks every commit from a git worktree: timing log hard-codes .git/ as a directory #2437 arch rejected that. A later aggregate series is a follow-up, not this fix.Acceptance criteria
.gitis a file), a hook run emits noNot a directoryon stderr..git/neotoma-hook-timings.log(the missing row / the stderr line, not merely that a git command was invoked).Priority and sequencing. Confidence: high. Assumption: #2437's per-worktree decision stands. Do not schedule a third implementation. Arch confirms the #2437 ADR rather than choosing a new path.
Open questions. None that block sequencing.
Engineering
Files to touch
.husky/pre-commit— only the line(s) computingNEOTOMA_HOOK_LOGand thehook_logwrite.Concrete change
Replace the hardcoded
.git/path with a per-worktree-safe resolution, consistent with the #2437 ADR (per-worktree logs, not--git-common-diraggregation):Use
git rev-parse --git-dir(NOT--git-common-dir) —--git-dirresolves to the per-worktree private dir (e.g..git/worktrees/<name>) in a linked worktree and to.gitin the main clone, giving each worktree its own log file, matching PM's "per-worktree, matching the #2437 decision" acceptance criterion.--git-common-dirwould collapse all worktrees onto one shared file, which arch already rejected.Keep
|| true(or equivalent) on every write throughhook_logexactly as-is — no change to the non-blocking guard.Data/contract changes
None. No schema, MCP, CLI, HTTP, or entity changes. Pure shell path resolution inside a git hook.
Layering
Single surface:
.husky/pre-commitonly. Do not touch any other hook file, gate stage, or shared script.Build steps
.husky/pre-commitonorigin/mainmatches the issue's quoted line (fallback default.git/neotoma-hook-timings.log); if Pre-commit hook blocks every commit from a git worktree: timing log hard-codes .git/ as a directory #2437 already landed a different fix, close this issue as satisfied by that PR and stop — do not implement a duplicate.git rev-parse --git-dirresolution above, preserving the${NEOTOMA_HOOK_LOG:-...}override pattern so callers can still set the env var explicitly.git worktree add.Not a directory..git/, in a distinct file from the worktree's..git/neotoma-hook-timings.logdefault (i.e. the assertion checks the row/stderr, not merely that a git command executed).npm run generate:test-catalog).closes #2468, with design basis line:Design basis: docs/foundation/principles.md#6-extend-the-mechanism-that-already-generalizes-do-not-build-a-parallel-one, and note in the PR body that this lands the Pre-commit hook blocks every commit from a git worktree: timing log hard-codes .git/ as a directory #2437-approved per-worktree resolution for this specific call site rather than introducing a new pattern.QA / Test Plan
Prior art. #2333, #2437, and this issue are the same
.git-as-file write. No implementing PR. Onorigin/main,.husky/pre-commitline 7 is stillNEOTOMA_HOOK_LOG="${NEOTOMA_HOOK_LOG:-.git/neotoma-hook-timings.log}". #2437 already names the eval file below. Do not add a second test.Surface. Husky pre-commit instrumentation only (
NEOTOMA_HOOK_LOGandhook_login.husky/pre-commit). Not MCP, HTTP, CLI, or schema. Noagentic_evalfixture and noeval:tier1. The vitest file is the gate; its CI result is the QA report.Eval.
tests/scripts/pre_commit_hook_timings_worktree.test.ts(absent). Command:npx vitest run tests/scripts/pre_commit_hook_timings_worktree.test.ts. Same PR regeneratesdocs/testing/automated_test_catalog.md(tests/scripts/is catalogued).Harness. Temp repo via real
git initandgit worktree add— do not mock.git.spawnSync("sh", ["-e", slice], { cwd })on the production block from# --- Hook instrumentationthrough the closing# ---------------------------------------------------------------------------, then onehook_logcall. Do not run type-check, lint, or the hook'snpm test. Do not reimplement the path logic in the test.Definition of done
.gitis a file),NEOTOMA_HOOK_LOGunset. Process stderr does not containNot a directory. The2>/dev/nullonprintfdoes not hide the shell's failed>>open; assert the child stderr.stageortotalatgit -C <worktree> rev-parse --git-path neotoma-hook-timings.log. The path is not<worktree>/.git/neotoma-hook-timings.log.--git-path, and that path is not the worktree path. This fails a--git-common-dirdefault (one shared file).sh -e. On currentorigin/main,hook_log's|| truealready returns 0 while stderr still printsNot a directoryand the worktree file is never created. A test that only checks exit status stays green on this bug..git/neotoma-hook-timings.log, the worktree case fails because stderr containsNot a directoryand/or the--git-pathfile gained no row — not because a git command was or was not invoked. Do not require the source to say--git-dirrather than--git-path. Either is acceptable when the two files differ.--git-common-diris not.NEOTOMA_HOOK_LOGset to a temp file: the row lands there and not in the git-dir log. The${NEOTOMA_HOOK_LOG:-...}form stays.NEOTOMA_HOOK_LOGpointed at a directory:sh -eexits 0.|| trueonhook_logstays. Type-check, lint, and test stages are unchanged.Run result. Not run. The file is not in the tree, so there is no CI link.
gate_status.qastayspendinguntil this test is committed and green.