Skip to content

fix(hooks): resolve track-edits.sh's PROJECT_DIR from the edited file's path#1982

Open
carlos-alm wants to merge 1 commit into
fix/issue-1836-update-graph-sh-hook-fallback-build-pathfrom
fix/issue-1838-bug-track-edits-sh-posttooluse-hook
Open

fix(hooks): resolve track-edits.sh's PROJECT_DIR from the edited file's path#1982
carlos-alm wants to merge 1 commit into
fix/issue-1836-update-graph-sh-hook-fallback-build-pathfrom
fix/issue-1838-bug-track-edits-sh-posttooluse-hook

Conversation

@carlos-alm

Copy link
Copy Markdown
Contributor

Summary

.claude/hooks/track-edits.sh (PostToolUse hook on Edit/Write, logs to .claude/session-edits.log so guard-git.sh can validate commits) resolved the 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. When it doesn't match, the edit is logged to the wrong worktree's log (or dropped as a ..-relative path), and guard-git.sh later blocks a legitimate commit with "These staged files were NOT edited in this session."

Fix

Derive PROJECT_DIR from the edited file's own path instead of the ambient cwd:

git -C "<nearest existing ancestor of dirname(FILE_PATH)>" rev-parse --show-toplevel

This mirrors the -C "$WORK_DIR" pattern guard-git.sh already 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 -C requires an existing path.

Applied to both .claude/hooks/track-edits.sh and its docs/examples/claude-code-hooks/track-edits.sh mirror (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 (via bash) with a piped tool_input payload and asserts the log lands in the correct worktree:

  • Fails against the pre-fix script (verified before implementing the fix) — the hook process's ambient cwd is a different git worktree than the one owning the edited file, and the log entry is dropped/misplaced.
  • Passes after the fix.
  • A second case covers Write targeting a not-yet-created nested directory (ancestor walk-up).

Test plan

  • npx vitest run tests/unit/hook-track-edits-worktree.test.ts — 2/2 pass
  • Confirmed both tests fail against the pre-fix script (reproduces the bug)
  • npx 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 no src/ changes from this PR — filed as bug: resolveByReceiver typeMap lookup lacks cross-language guard, unlike resolveByGlobal #1981
  • npm run lint — clean on touched files
  • shellcheck — clean on both hook script copies

Fixes #1838

…'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-apps

greptile-apps Bot commented Jul 7, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR fixes edit tracking so logs follow the edited file's worktree. The main changes are:

  • Resolve the hook project directory from the edited file path.
  • Mirror the hook update in the documented example.
  • Add tests for cross-worktree cwd mismatch and nested Write targets.

Confidence Score: 5/5

This looks safe to merge after a small portability cleanup.

  • The worktree resolution logic matches the intended hook and guard flow.
  • The only issue found is limited to environments where dirname -- is unavailable.
  • The added tests cover the main edited-file worktree behavior.

.claude/hooks/track-edits.sh and docs/examples/claude-code-hooks/track-edits.sh

Important Files Changed

Filename Overview
.claude/hooks/track-edits.sh Updates the production hook to resolve the project from the edited file path; the new dirname -- use is a portability risk.
docs/examples/claude-code-hooks/track-edits.sh Mirrors the production hook change; the same dirname -- portability issue can affect copied examples.
tests/unit/hook-track-edits-worktree.test.ts Adds bash-level tests for logging to the edited file's worktree and walking up from missing nested directories.

Fix All in Claude Code

Reviews (1): Last reviewed commit: "fix(hooks): resolve track-edits.sh's PRO..." | Re-trigger Greptile

Comment on lines +35 to +38
SEARCH_DIR=$(dirname -- "$FILE_PATH")
while [ ! -d "$SEARCH_DIR" ] && [ "$SEARCH_DIR" != "/" ] && [ -n "$SEARCH_DIR" ]; do
SEARCH_DIR=$(dirname -- "$SEARCH_DIR")
done

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Nonportable Dirname Option

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.

Suggested change
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

Fix in Claude Code

Comment on lines +35 to +38
SEARCH_DIR=$(dirname -- "$FILE_PATH")
while [ ! -d "$SEARCH_DIR" ] && [ "$SEARCH_DIR" != "/" ] && [ -n "$SEARCH_DIR" ]; do
SEARCH_DIR=$(dirname -- "$SEARCH_DIR")
done

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 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.

Suggested change
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

Fix in Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant