code-review-graph version
2.3.8
Operating system
Linux
Python version
3.11.15
AI platform
claude-code
Output of code-review-graph status
$ code-review-graph status # main working tree
Nodes: 14985
Edges: 137385
Files: 997
Languages: bash, javascript, python, tsx, typescript
Last updated: 2026-09-01T19:05:40
Built on branch: fix/review-graph-worktree-and-search-mode
Built at commit: 050fd933dcc7
$ code-review-graph status # from a linked worktree, before the hook ran
Nodes: 0
Steps to reproduce
code-review-graph install in the main working tree of a repo (this writes pre-commit — see below).
code-review-graph build in that main working tree. Note the node/file counts.
git worktree add ../wt -b some-branch
cd ../wt, change one file, git commit.
ls ../wt/.code-review-graph — a second, complete database is now there.
code-review-graph status from ../wt reports that graph, not the one from step 2.
Expected vs actual behavior
Expected: committing from a linked worktree updates the graph the repository already has, or declines and says why. It should not silently create a second store.
Actual: a full build runs inside the worktree and leaves a complete second database there — 267 MB in my case, on a 997-file repo. Subsequent queries from that worktree answer from whichever store cwd-discovery finds, reporting "status": "ok" either way.
Additional context
Cause. The hook template in code_review_graph/skills.py:1152-1158 passes no --repo:
#!/bin/sh
# Installed by code-review-graph. Remove this file to disable pre-commit graph checks.
if command -v code-review-graph >/dev/null 2>&1; then
code-review-graph update || true
code-review-graph detect-changes --brief || true
fi
Both commands then discover the repository from the working directory, which at commit time is the tree being committed. The surrounding installer is careful about where the hook lands — the docstring calls out linked worktrees, submodules and core.hooksPath explicitly (#313), and it resolves via git rev-parse --git-path hooks. So the hook is correctly placed to run inside worktrees, and then misbehaves in exactly those worktrees. Because the resolved path is usually the common dir, one hook file serves every worktree and one fix covers all of them.
repo_root is already in scope at install time, so the information needed is available; it just isn't carried into the script.
Why this is worth fixing rather than documenting. The failure mode is a confident wrong answer, not an error. A freshly created store in a worktree contains only what that worktree has touched, and a graph that has never parsed a file returns 0 for it — indistinguishable from a true 0. callers_of on a live symbol reports no callers, which reads as "nothing depends on this, safe to change". I measured four worktrees on one repo holding 7, 27, 26 and 6 files against a real 967, every one answering "status": "ok".
Not a duplicate, as far as I can tell — I looked at the three closest:
docs/TROUBLESHOOTING.md describes where the hook is placed (including worktrees) but not this behavior.
Suggested fix, if useful — resolving at runtime rather than baking an absolute path in, so the hook survives the repo being moved:
#!/bin/sh
# Installed by code-review-graph. Remove this file to disable pre-commit graph checks.
command -v code-review-graph >/dev/null 2>&1 || exit 0
root=$(git worktree list --porcelain 2>/dev/null | awk '/^worktree /{print $2; exit}')
[ -n "$root" ] || exit 0
here=$(git rev-parse --show-toplevel 2>/dev/null)
gitdir=$(git rev-parse --absolute-git-dir 2>/dev/null)
[ -n "$here" ] && [ -n "$gitdir" ] || exit 0
code-review-graph update --repo "$root" >/dev/null 2>&1 || true
GIT_DIR="$gitdir" GIT_WORK_TREE="$here" \
code-review-graph detect-changes --repo "$root" --brief || true
The first git worktree list entry is always the main working tree. The two calls need opposite things, which is the part worth keeping if you take a different shape: update should write the shared store, while detect-changes must read this branch's diff — --repo selects both the graph and the tree the diff is read from, so without GIT_DIR/GIT_WORK_TREE it reports on the main branch and prints "no changes detected" on a branch full of them.
I have been running the above locally and can confirm a second commit from a worktree creates no store there and leaves the shared graph correct (14,985 nodes throughout). Happy to open a PR if you'd like it in this shape; equally happy if you'd rather the hook simply refuse to run outside the main working tree, which is a smaller change and also removes the wrong answer.
code-review-graph version
2.3.8
Operating system
Linux
Python version
3.11.15
AI platform
claude-code
Output of
code-review-graph statusSteps to reproduce
code-review-graph installin the main working tree of a repo (this writespre-commit— see below).code-review-graph buildin that main working tree. Note the node/file counts.git worktree add ../wt -b some-branchcd ../wt, change one file,git commit.ls ../wt/.code-review-graph— a second, complete database is now there.code-review-graph statusfrom../wtreports that graph, not the one from step 2.Expected vs actual behavior
Expected: committing from a linked worktree updates the graph the repository already has, or declines and says why. It should not silently create a second store.
Actual: a full build runs inside the worktree and leaves a complete second database there — 267 MB in my case, on a 997-file repo. Subsequent queries from that worktree answer from whichever store cwd-discovery finds, reporting
"status": "ok"either way.Additional context
Cause. The hook template in
code_review_graph/skills.py:1152-1158passes no--repo:Both commands then discover the repository from the working directory, which at commit time is the tree being committed. The surrounding installer is careful about where the hook lands — the docstring calls out linked worktrees, submodules and
core.hooksPathexplicitly (#313), and it resolves viagit rev-parse --git-path hooks. So the hook is correctly placed to run inside worktrees, and then misbehaves in exactly those worktrees. Because the resolved path is usually the common dir, one hook file serves every worktree and one fix covers all of them.repo_rootis already in scope at install time, so the information needed is available; it just isn't carried into the script.Why this is worth fixing rather than documenting. The failure mode is a confident wrong answer, not an error. A freshly created store in a worktree contains only what that worktree has touched, and a graph that has never parsed a file returns
0for it — indistinguishable from a true0.callers_ofon a live symbol reports no callers, which reads as "nothing depends on this, safe to change". I measured four worktrees on one repo holding 7, 27, 26 and 6 files against a real 967, every one answering"status": "ok".Not a duplicate, as far as I can tell — I looked at the three closest:
update's automatic full-rebuild fallback bypassing the root-mismatch guard from A relative --repo wipes the graph: reconciliation removes every file when the root spelling differs from the build #889 and replacing a graph anchored elsewhere. Mine creates a brand-new store where none existed, so there is nothing for that guard to mismatch against; it is a different path to a different outcome.okon a cold worktree. Same family of symptom, but that is the read side, and it does not involve the installed hook.docs/TROUBLESHOOTING.mddescribes where the hook is placed (including worktrees) but not this behavior.Suggested fix, if useful — resolving at runtime rather than baking an absolute path in, so the hook survives the repo being moved:
The first
git worktree listentry is always the main working tree. The two calls need opposite things, which is the part worth keeping if you take a different shape:updateshould write the shared store, whiledetect-changesmust read this branch's diff —--reposelects both the graph and the tree the diff is read from, so withoutGIT_DIR/GIT_WORK_TREEit reports on the main branch and prints "no changes detected" on a branch full of them.I have been running the above locally and can confirm a second commit from a worktree creates no store there and leaves the shared graph correct (14,985 nodes throughout). Happy to open a PR if you'd like it in this shape; equally happy if you'd rather the hook simply refuse to run outside the main working tree, which is a smaller change and also removes the wrong answer.