fix(hooks): resolve track-edits.sh's PROJECT_DIR from the edited file's path#1982
Conversation
…'s path track-edits.sh resolved the session-edits.log location via a bare `git rev-parse --show-toplevel`, relying on the hook process's own ambient cwd. Edit/Write tool calls carry only an absolute file_path with no associated "current directory" state, so that ambient cwd is not guaranteed to match the worktree that actually owns the edited file — causing edits to be logged to the wrong worktree (or dropped) and guard-git.sh to later block legitimate commits with "NOT edited in this session". Derive PROJECT_DIR from the edited file's own path instead (`git -C "<dir containing file_path>" rev-parse --show-toplevel`, walking up to the nearest existing ancestor for not-yet-created Write targets), mirroring the `-C "$WORK_DIR"` pattern guard-git.sh already uses on the read side of this same check. Applied to both the live hook and its docs/examples mirror. Fixes #1838
Greptile SummaryThis PR fixes edit tracking so logs follow the edited file's worktree. The main changes are:
Confidence Score: 5/5This looks safe to merge after a small portability cleanup.
.claude/hooks/track-edits.sh and docs/examples/claude-code-hooks/track-edits.sh Important Files Changed
Reviews (1): Last reviewed commit: "fix(hooks): resolve track-edits.sh's PRO..." | Re-trigger Greptile |
| SEARCH_DIR=$(dirname -- "$FILE_PATH") | ||
| while [ ! -d "$SEARCH_DIR" ] && [ "$SEARCH_DIR" != "/" ] && [ -n "$SEARCH_DIR" ]; do | ||
| SEARCH_DIR=$(dirname -- "$SEARCH_DIR") | ||
| done |
There was a problem hiding this comment.
When this hook runs on a shell environment whose dirname does not accept the non-POSIX -- marker, set -e makes the script exit at the first path split. The edit is not written to .claude/session-edits.log, so a later guarded commit can reject a file that was actually edited in the session.
| SEARCH_DIR=$(dirname -- "$FILE_PATH") | |
| while [ ! -d "$SEARCH_DIR" ] && [ "$SEARCH_DIR" != "/" ] && [ -n "$SEARCH_DIR" ]; do | |
| SEARCH_DIR=$(dirname -- "$SEARCH_DIR") | |
| done | |
| SEARCH_DIR=$(dirname "$FILE_PATH") | |
| while [ ! -d "$SEARCH_DIR" ] && [ "$SEARCH_DIR" != "/" ] && [ -n "$SEARCH_DIR" ]; do | |
| SEARCH_DIR=$(dirname "$SEARCH_DIR") | |
| done |
| SEARCH_DIR=$(dirname -- "$FILE_PATH") | ||
| while [ ! -d "$SEARCH_DIR" ] && [ "$SEARCH_DIR" != "/" ] && [ -n "$SEARCH_DIR" ]; do | ||
| SEARCH_DIR=$(dirname -- "$SEARCH_DIR") | ||
| done |
There was a problem hiding this comment.
Example Uses Nonportable Dirname
The documented hook copy now depends on dirname --, while the hook docs do not require a GNU-compatible dirname. Repos that copy this example onto an environment without that option will fail before logging the edit, leaving the commit guard unable to match the edited file.
| SEARCH_DIR=$(dirname -- "$FILE_PATH") | |
| while [ ! -d "$SEARCH_DIR" ] && [ "$SEARCH_DIR" != "/" ] && [ -n "$SEARCH_DIR" ]; do | |
| SEARCH_DIR=$(dirname -- "$SEARCH_DIR") | |
| done | |
| SEARCH_DIR=$(dirname "$FILE_PATH") | |
| while [ ! -d "$SEARCH_DIR" ] && [ "$SEARCH_DIR" != "/" ] && [ -n "$SEARCH_DIR" ]; do | |
| SEARCH_DIR=$(dirname "$SEARCH_DIR") | |
| done |
Summary
.claude/hooks/track-edits.sh(PostToolUse hook on Edit/Write, logs to.claude/session-edits.logsoguard-git.shcan validate commits) resolved the log location via a baregit rev-parse --show-toplevel— relying on the hook process's own ambient cwd.Edit/Write tool calls carry only an absolute
file_pathwith no associated "current directory" state, so that ambient cwd is not guaranteed to match the worktree that actually owns the edited file. When it doesn't match, the edit is logged to the wrong worktree's log (or dropped as a..-relative path), andguard-git.shlater blocks a legitimate commit with "These staged files were NOT edited in this session."Fix
Derive
PROJECT_DIRfrom the edited file's own path instead of the ambient cwd:git -C "<nearest existing ancestor of dirname(FILE_PATH)>" rev-parse --show-toplevelThis mirrors the
-C "$WORK_DIR"patternguard-git.shalready uses on the read side of this same check (resolving the commit-target worktree from the git command itself rather than ambient cwd).Walks up to the nearest existing ancestor directory first, since the Write tool can target a not-yet-created nested directory and
git -Crequires an existing path.Applied to both
.claude/hooks/track-edits.shand itsdocs/examples/claude-code-hooks/track-edits.shmirror (the documented copy for other repos adopting these hooks) so the example doesn't propagate the same bug.Tests
Added
tests/unit/hook-track-edits-worktree.test.ts, which actually invokes the hook script (viabash) with a pipedtool_inputpayload and asserts the log lands in the correct worktree:Test plan
npx vitest run tests/unit/hook-track-edits-worktree.test.ts— 2/2 passnpx vitest run tests/unit/— 52/53 files pass; the one failing file (call-resolver.test.ts, 2 tests) is a pre-existing, unrelated failure on this branch with nosrc/changes from this PR — filed as bug: resolveByReceiver typeMap lookup lacks cross-language guard, unlike resolveByGlobal #1981npm run lint— clean on touched filesshellcheck— clean on both hook script copiesFixes #1838