Skip to content

fix: hooks follow the agent's cwd — subdirectories and git worktrees - #5

Merged
aoc81 merged 2 commits into
mainfrom
fix/hooks-follow-agent-cwd
Sep 30, 2026
Merged

aoc81 merged 2 commits into
mainfrom
fix/hooks-follow-agent-cwd

Conversation

@aoc81

@aoc81 aoc81 commented Sep 30, 2026

Copy link
Copy Markdown
Owner

Summary

Claude Code documents that the hook input's cwd follows the agent: it is the new directory after a cd, and the worktree root once the agent enters a worktree. Two places assumed hooks always run at the project root:

  • Subdirectories (6327f82): find_project_dir took cwd literally. After cd src/, or with claude launched from a monorepo package, recall, edit-time recall, the capture nudge (including captureNudge: "off"), the post-compaction reminder and /lore:lint|scan|mine all looked for the store in the wrong directory and went silent. It now searches upward for the nearest .lore.json or learnings/, bounded by the enclosing git repository. Outside a repository only the start directory is checked, as before.
  • Git worktrees (f0425f6): the edit-time dedupe state and the recall log required .git to be a directory. In a linked worktree .git is a gitdir: file, so the same learning was re-injected on every edit and nothing was logged. git_dirs() resolves the pointer by reading it from disk (no subprocess in the hooks). Dedupe state is kept per worktree; the recall log goes in the shared .git, so /lore:stats sees every worktree.

Test plan

  • New tests reproduce both bugs; the worktree ones use a real git worktree add. They fail on main and pass here.
  • python -m unittest discover -s tests: 182 passed, 1 skipped.
  • Merges cleanly with fix/skill-script-paths and fix/linter-blind-spots in any order; the combined suite is green.

🤖 Generated with Claude Code

https://claude.ai/code/session_01DMXx6BJswH6xiwdqrwQnEf


Generated by Claude Code

`find_project_dir` took the hook's `cwd` as the project root. Claude Code
documents that `cwd` follows the agent: after a `cd src/` it is the
subdirectory, and `claude` launched from a monorepo package starts there too.
The store and `.lore.json` were then looked up in the wrong directory, so:

    cwd = <repo>                 recall surfaces learnings/ci/cache.md
    cwd = <repo>/src/deep        recall prints nothing -- even with
                                 CLAUDE_PROJECT_DIR set to <repo>

Everything built on it went quiet the same way: edit-time recall, the
capture nudge (which also stopped honoring `captureNudge: "off"`, since the
config was never found), the post-compaction reminder, and /lore:lint,
/lore:scan and /lore:mine run from a subdirectory.

Search upward from the start directory for the nearest `.lore.json` or
`learnings/` instead. `CLAUDE_PROJECT_DIR` is not used as the answer on
purpose: it stays on the original checkout when the agent enters a git
worktree, and reading that checkout's store would break path matching for
files edited in the worktree.

The walk is bounded: it never leaves the enclosing git repository (a stray
`~/learnings` must not answer for an unrelated project), and outside any
repository only the start directory is checked, which is the old behavior.

Tests: the reproduction above is a hook-level test, plus unit tests for the
marker lookup, the `.lore.json` marker, the repository bound, the no-repo
case and the $CLAUDE_PROJECT_DIR start. The four that exercise the bug fail
without the change; the two bound tests pin behavior that must not regress.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DMXx6BJswH6xiwdqrwQnEf
Both kept their state under `<project>/.git` and bailed unless that was a
directory. In a linked worktree -- `git worktree add`, which is how parallel
agents usually run -- `.git` is a one-line FILE (`gitdir: <path>`), so:

    edit 1 in the worktree       learning injected
    edit 2, same session         injected again (and on every edit after)
    .git/lore-recall.log         never written: /lore:stats saw nothing

Resolve the pointer instead (`git_dirs` in _common): the worktree's private
git dir holds the dedupe state, so agents in separate worktrees never reset
each other's; its `commondir` names the repository's shared `.git`, which
holds the recall log, so every worktree feeds the same /lore:stats numbers and
an ephemeral worktree takes none of them with it when it is removed. A
submodule's relative `gitdir:` with no `commondir` resolves to its own dir.
It reads the pointer from disk rather than running `git rev-parse`, because
the hooks run on every prompt and every edit.

Tests: a real `git worktree add` drives the edit-time hook twice (dedupe) and
checks the shared log; the linter reads that log from a worktree; unit tests
cover plain, worktree, submodule and malformed pointers. The three behavior
tests fail without the change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01DMXx6BJswH6xiwdqrwQnEf
@aoc81
aoc81 merged commit c11ed33 into main Sep 30, 2026
6 checks passed
@aoc81 aoc81 mentioned this pull request Sep 30, 2026
2 of 3 tasks
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.

2 participants