hooks: keep the rebuild root inside the repo (#3265) - #3492
Conversation
Team setup documents committing the graphify output directory, and .graphify_root lives inside it, so its contents are checkout controlled. Both generated git hooks read that file and pass its value straight to _rebuild_code with no bound, so a value planted there by a forked PR could point the rebuild, and therefore what it reads and what it writes back into that same committed directory, at a location outside the repository the hook runs in. Both rebuild bodies now only adopt the saved root when it resolves inside the working tree the hook is running from, falling back to the repo top otherwise. Fixes Graphify-Labs#3265. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
Executes the shipped snippet directly (extracted from the rebuild body text) rather than a hand copy that could quietly drift from it, covering both the rejected case (a marker pointing outside the repo) and the case the guard must not break (a subdirectory scoped root). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
There was a problem hiding this comment.
Graphify reviewed this change.
Worth a look — the grounded gate found no coupling regressions or blocking issues, but 2 advisory finding(s) below merit a look before merge.
Graphify review — findings
Constrains the .graphify_root value recovered by the generated post-commit and post-checkout hooks to paths inside the working tree the hook runs from, so a checkout-controlled root planted in the committed graphify-out/ can no longer redirect where the rebuild reads and writes; out-of-repo values are ignored with a printed notice and the hook falls back to .. Subdirectory-scoped roots inside the repo are still honoured. Adds parametrized tests that execute the shipped resolution snippet extracted straight from the rebuild bodies, covering both the rejected out-of-repo case and the preserved in-repo scoped case.
Worth a look
- Symlink-loop .graphify_root can crash generated hooks —
graphify/hooks.py:194· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Checkout hook validated path is reopened after symlink race —
graphify/hooks.py:272· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 396 functions depend on the 211 functions this change touches.
Health — this change adds coupling hotspots:
- new:
install()— 37 callers, 7 callees - new:
dispatch_command()— 2 callers, 124 callees - new:
dispatch_install_cli()— 2 callers, 31 callees - new:
status()— 8 callers, 6 callees - new:
uninstall()— 9 callers, 5 callees - new:
uninstall_all()— 2 callers, 13 callees - new:
test_poisoned_manifest_is_healed()— 0 callers, 6 callees
Verification — 396 functions in the blast radius were not formally verified this run (proofs are advisory here).
Gate & verification
graphify gate
PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.
Advisory (not blocking):
- verification_scope: 226 function(s) in the blast radius were not formally verified this run
Test selection
Test selection
21 of 268 test file(s) selected (8%) via static blast radius.
tests/test_affected_cli.py— impacttests/test_agents_platform.py— impacttests/test_codebuddy.py— impacttests/test_devin.py— impacttests/test_explain_cli.py— impacttests/test_extract_cli.py— impacttests/test_global_add_tag_inference.py— impacttests/test_god_nodes_cli.py— impacttests/test_hollow_chunks_arm_shrink_guard.py— impacttests/test_hook_chain_survives_skip.py— impacttests/test_hooks.py— impact, changed-testtests/test_incomplete_build_guard.py— impacttests/test_install.py— impacttests/test_install_references.py— impacttests/test_merge_chunks_validation.py— impacttests/test_multigraph_diagnostics.py— impacttests/test_no_dedup_flag.py— impacttests/test_path_cli.py— impacttests/test_query_cli.py— impacttests/test_query_induced_edges.py— impacttests/test_unverified_semantic_shrink.py— impact
Selection is safe under the controlled-regression assumption; always-run tests + a periodic full run are the backstops. Advisory — it never changes the check verdict.
· 7 more finding(s) on lines outside this diff (see the check run).
Path.resolve() raises RuntimeError, not OSError, when a symlink chain loops back on itself, so the Graphify-Labs#3265 in repo check let that propagate up instead of falling back to the repo top like every other bad marker value. Since a fork or PR can commit an actual symlink loop alongside a crafted .graphify_root value, an untrusted checkout could still turn the guard itself into a failure path. Both rebuild bodies now catch RuntimeError alongside OSError. Found by the graphify review bot on PR 3492. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh
|
Thanks for the review. "Symlink-loop .graphify_root can crash generated hooks" — confirmed and fixed. Reproduced directly: "Checkout hook validated path is reopened after symlink race" — looked at this, leaving it as is. This is a TOCTOU between the |
There was a problem hiding this comment.
Graphify reviewed this change.
Worth a look — the grounded gate found no coupling regressions or blocking issues, but 1 advisory finding(s) below merit a look before merge.
Graphify review — findings
Constrains the .graphify_root recovered from a checkout-committed file to paths inside the working tree the hook runs from: an absolute or out-of-repo value is now ignored (with a printed notice) in favor of the repo root, while a same-repo subdirectory scope is still honored. Falls back to the repo top when Path.resolve() can't classify the candidate, catching both OSError and the RuntimeError a symlink loop raises. Adds parametrized post-commit/post-checkout tests that execute the shipped resolution snippet itself for the out-of-repo, in-repo-scoped, and symlink-loop cases.
Worth a look
- Out-of-repo .graphify_root is no longer honored by installed hooks —
graphify/hooks.py:195· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
Analysis details — impact, health, verification
Impact & health
Graphify review
Impact — 398 functions depend on the 213 functions this change touches.
Health — this change adds coupling hotspots:
- new:
install()— 37 callers, 7 callees - new:
dispatch_command()— 2 callers, 124 callees - new:
dispatch_install_cli()— 2 callers, 31 callees - new:
status()— 8 callers, 6 callees - new:
uninstall()— 9 callers, 5 callees - new:
uninstall_all()— 2 callers, 13 callees - new:
test_poisoned_manifest_is_healed()— 0 callers, 6 callees
Verification — 398 functions in the blast radius were not formally verified this run (proofs are advisory here).
Gate & verification
graphify gate
PASS — objectively clean (no health regressions, tests not run — proofs not run this pass (advisory)). Grounded, not self-assessed.
Advisory (not blocking):
- verification_scope: 228 function(s) in the blast radius were not formally verified this run
Test selection
Test selection
21 of 268 test file(s) selected (8%) via static blast radius.
tests/test_affected_cli.py— impacttests/test_agents_platform.py— impacttests/test_codebuddy.py— impacttests/test_devin.py— impacttests/test_explain_cli.py— impacttests/test_extract_cli.py— impacttests/test_global_add_tag_inference.py— impacttests/test_god_nodes_cli.py— impacttests/test_hollow_chunks_arm_shrink_guard.py— impacttests/test_hook_chain_survives_skip.py— impacttests/test_hooks.py— impact, changed-testtests/test_incomplete_build_guard.py— impacttests/test_install.py— impacttests/test_install_references.py— impacttests/test_merge_chunks_validation.py— impacttests/test_multigraph_diagnostics.py— impacttests/test_no_dedup_flag.py— impacttests/test_path_cli.py— impacttests/test_query_cli.py— impacttests/test_query_induced_edges.py— impacttests/test_unverified_semantic_shrink.py— impact
Selection is safe under the controlled-regression assumption; always-run tests + a periodic full run are the backstops. Advisory — it never changes the check verdict.
· 7 more finding(s) on lines outside this diff (see the check run).
|
Re "Out-of-repo .graphify_root is no longer honored by installed hooks" — that's the intended fix, not a regression. #3265 is exactly about an out-of-repo |
|
Shipped in v0.9.59 — landed on |
- chinese extra: pin jieba-py from 3.12 (was 3.14); jieba's invalid escapes are a hard error on 3.12+ and broke graphify.serve import. - serve.py: suppress the tokenizer SyntaxWarning by category so -W error can't escalate it; catch SyntaxError as a fallback. - hooks.py: harden the #3492 rebuild-root guard for 3.13, whose resolve() no longer raises on a symlink loop — require a real directory. Verified full suite green on both 3.10 (5628) and 3.13 (5627).
Summary
Fixes #3265.
.graphify_rootlives inside the directory Team setup documents committing, so its contents are checkout controlled — but the generated post-commit / post-checkout hooks read that file and pass its value straight to_rebuild_codewith no bound. A value planted there by a malicious fork or PR (an absolute path outside the repository) would steer the rebuild, and therefore what it reads and what it writes back into that same committed directory, wherever the checkout names, not the repository the hook was installed into.Both rebuild bodies (
_REBUILD_BODY_COMMITand_REBUILD_BODY_CHECKOUTingraphify/hooks.py) now only adopt the saved root when it resolves inside the working tree the hook is actually running from (_root.resolve() == cwdor a descendant of it); anything else falls back to the repo top, the previous default when the marker was absent, and prints a note saying so.This does not narrow the legitimate case: a subdirectory scoped root (#1173, e.g.
graphify update backend/) is still a descendant of the repo root the hook runs from, so it's still honoured — covered by a new test.This PR only addresses the
.graphify_roothalf of the report. The reporter's earlier.graphify_pythonfollow-up explicitly asks for a maintainer preference among three directions (drop the working tree probe, relocate it outside the repo, or document it in.gitignore), so I've left that to a maintainer call rather than guessing.Test plan
tests/test_hooks.py: one confirms a.graphify_rootpointing outside the repo is rejected and falls back to the repo top, one confirms a subdirectory scoped root (post-commit hook overwrites .graphify_root, silently expanding a scoped build to the whole repo #1173) is still honoured. Both execute the actual shipped snippet (extracted from the body text via regex) rather than a hand copy, so they can't drift from the real hook source..graphify_rootpointing at a directory outside the repo, confirmed the recovered root falls back to the repo top; wrote a subdirectory value, confirmed it's still honoured.python3 -m pytest -q— 5424 passed, only the pre-existing unrelated failures (test_ollama_retry_cap.pymissingopenaiin this env, one flaky timing assertion intest_ts_import_type_arguments.py).python3 -m tools.skillgen --check— OK.🤖 Generated with Claude Code
https://claude.ai/code/session_017qfdzgbA5KedGEjD1AayNh