Skip to content

fix(install): emit a bare hook command for the default install too (#3129) - #3268

Open
yotamleo wants to merge 1 commit into
Graphify-Labs:v8from
yotamleo:fix/codex-hook-portable-command-default-install
Open

fix(install): emit a bare hook command for the default install too (#3129)#3268
yotamleo wants to merge 1 commit into
Graphify-Labs:v8from
yotamleo:fix/codex-hook-portable-command-default-install

Conversation

@yotamleo

@yotamleo yotamleo commented Sep 1, 2026

Copy link
Copy Markdown

Summary

v0.9.52 (#3149, thanks @davidbhoward for the original analysis) fixed
graphify install --project: a committed, shared install now emits a bare,
run-time-resolved graphify command instead of pinning an absolute
interpreter path. That PR was deliberately scoped to --project only, and
said so explicitly:

This PR also does not address the non---project half of #3129 ... the
absolute path is defensible there ... and the case is further entangled
with #2164 ... Left for a maintainer call rather than silently bundled.

This is the deferred half, offered as the patch mentioned on the issue thread
on 2026-09-01. It only extends #3149's own fix to the case its own scope
section named as open.

The remaining defect

_install_codex_hook, _claude_pretooluse_hooks (claude + codebuddy) and
_gemini_hook all write into project_dir, which defaults to the current
directory -- whether or not --project was passed. There is no separate
user-profile destination for these specific hook files today; only the flag
that picked which exe path got resolved into them differed. So a plain
graphify codex install (no flag) still wrote a machine-absolute path
(e.g. C:/Users/<user>/.local/bin/graphify.EXE hook-check) into
.codex/hooks.json, and if that gets committed -- which is exactly what the
issue's own reproduction does -- it is wrong on every other machine and in CI.

Checked whether this was codex-specific, per the discussion on the issue: it
is not. claude_install, gemini_install and codebuddy_install all resolve
their hook destination the same way (project_dir or Path("."), no
distinction by scope), so all three shared the identical defect --
_claude_pretooluse_hooks is used by both claude and codebuddy, and codebuddy
has no --project variant at all, meaning every codebuddy install hit this.

The fix

Small and destination-keyed, not flag-keyed: since these particular hook files
are always written into the current directory regardless of --project
today, the exe they embed is now always the bare graphify command,
independent of that flag. The project parameter #3149 threaded through these
three functions is dropped since it no longer changes the outcome for them;
_resolve_graphify_exe itself, and its absolute-path branch, are untouched.

Why this doesn't preempt #2164

#2164 is about where the non-project install writes (cwd vs. a real
user-profile directory). This PR doesn't touch that -- it only changes what
gets embedded in the file at whatever destination is used today, which is
unconditionally the project directory. If #2164 is ever fixed so the
non-project install gets a genuine, non-shared destination, _resolve_graphify_exe's
absolute-path branch (left exactly as-is here) is what that destination should
resolve to -- that's the point to reintroduce a project/non-project
distinction on these three call sites, not before. This PR should need no
rework when that lands; it just stops being the only caller of that branch's
bare-command side.

The cost this pays, stated plainly

There is a real trade-off here and it deserves naming rather than burying,
because _resolve_graphify_exe's own docstring makes the case against this
change:

Using an absolute path ensures the hook works in environments where the venv
Scripts/ directory is not on PATH (e.g. VS Code Codex extension on Windows).

That is true, and this PR gives it up for these three hook files. On a machine
where graphify is not on PATH, a bare command fails where the absolute path
would have worked.

I think #3149 already answered this, and the answer now covers more ground than
it did when written:

A missing binary and a wrong absolute path both fail the hook; the difference
is diagnosis. graphify: command not found names the problem, whereas a path
into another developer's home directory gives the reader nothing to work with
and reads like a deliberate configuration choice. ... This argument applies
only to the committed case, which is why the user-profile path is left alone.

The last sentence is the hinge. That argument was scoped to "the committed
case" because the non-project install was assumed to have a different,
uncommitted destination. For these three hook files it does not — every
install writes them into the working directory — so every case is the
committed case, and #3149's own reasoning extends to all of them.

If you would rather keep the absolute path for the PATH-fragile environments
and accept the churn, the alternative is (2) from the issue thread: stop
writing these files into a tracked location for non-project installs. That is
the larger change and it overlaps #2164, which is presumably why you deferred
it — I have not attempted it here.

One consequence worth flagging for review

After this change all three call sites pass _resolve_graphify_exe(project=True),
so its absolute-path branch has no live caller. I deliberately did not
delete it: #2164 is exactly the change that should revive it, and removing it
now would mean re-deriving the Windows/PATH reasoning later from the git log.
But it does mean the parameter now reads oddly at the call sites — a
non-project install asking for project=True. If you take this, a rename
(bare=True, or an explicit _bare_graphify_command() helper) would say what
is actually meant; I left the signature alone to keep the diff reviewable and
because naming is yours to pick.

Scope

Matches #3149's own boundary: the trailing-newline and
customized-hook-clobber issues it flagged as deliberately out of scope are
still untouched here.

This is an offer, not a demand

Per the maintainer's own framing on the issue (partial fix landed, remainder
"left for a maintainer call"), this follows the "(1)" option floated on the
thread -- happy to close this in favor of a different design, or adjust scope,
if a different approach is preferred.

Refs #3129.

Verification

Re-verified the defect at current v8 HEAD (33362d9, "chore: bump to
0.9.53") before writing any code: graphify codex install (no --project)
still writes _resolve_graphify_exe(project=False)'s absolute result into
.codex/hooks.json in the cwd; claude_install/gemini_install/codebuddy_install
all showed the same project_dir or Path(".") pattern.

Red first. Added/renamed tests in tests/test_install.py
(test_default_install_hook_command_is_also_portable, parametrized over
claude/codex/gemini, plus test_codebuddy_hook_command_is_portable), and
confirmed they fail against unmodified v8:

python -m pytest tests/test_install.py -k "test_default_install_hook_command_is_also_portable or test_codebuddy_hook_command_is_portable" -v
...
FAILED tests/test_install.py::test_codebuddy_hook_command_is_portable
FAILED tests/test_install.py::test_default_install_hook_command_is_also_portable[claude]
FAILED tests/test_install.py::test_default_install_hook_command_is_also_portable[codex]
FAILED tests/test_install.py::test_default_install_hook_command_is_also_portable[gemini]
4 failed, 97 deselected

All four pass after the fix.

Existing #3149 test conflict, resolved explicitly, not silently.
test_user_profile_install_still_resolves_absolute_path pinned the premise
that the non-project install has a separate, non-shared destination. That
premise doesn't hold for these three writers (see above), so the fix
necessarily changes what that test asserts. Rather than delete or quietly
tweak it, it's replaced by test_default_install_hook_command_is_also_portable
with a docstring stating exactly why the old test's premise no longer holds
and cross-referencing this PR, plus a module comment explaining the
supersession for anyone reading the test file later.

Scoped run:

python -m pytest tests/test_install.py tests/test_settings_merge.py tests/test_install_roundtrip.py -q
171 passed, 3 failed

The 3 failures (test_codex_skill_uses_graphify_with_existing_graph,
test_hermes_skill_destination_posix_uses_home,
test_skill_roundtrip_at_real_destination[user-hermes]) are pre-existing and
unrelated (Windows LOCALAPPDATA/hermes path handling) -- confirmed present,
identical, on a clean v8 checkout before this change (170 passed, 3 failed;
the one extra passing test on the fixed branch is the added
test_codebuddy_hook_command_is_portable).

Full suite, both branches, failure sets diffed:

python -m pytest -q   # this branch:   30 failed, 4950 passed, 283 skipped
python -m pytest -q   # clean v8:      30 failed, 4949 passed, 283 skipped
diff <(sort fixed_failures.txt) <(sort baseline_failures.txt)   # empty

Identical 30-test failure set on both (symlinks/fifo/unix-socket, hermes/LOCALAPPDATA,
ollama retry-cap, terraform parser, ambient-environment tests unrelated to
install.py) -- zero introduced, zero fixed. Windows-only run; the Ubuntu CI
matrix is exact parity per the README.

Lint / CI gates:

ruff check graphify/install.py tests/test_install.py   # All checks passed!
python -m tools.skillgen --check                        # check OK: 134 artifact(s) match
python -m tools.skillgen --audit-coverage                # OK
python -m tools.skillgen --schema-singleton              # OK
python -m tools.skillgen --monolith-roundtrip             # OK
python -m tools.skillgen --always-on-roundtrip            # OK

(install.py is not a skillgen-generated file; ran the gate anyway since it's
a blocking CI job.)

…raphify-Labs#3129)

v0.9.52 (Graphify-Labs#3149) fixed --project installs so the committed hook config
carries a bare `graphify` command instead of a machine-absolute path.
The plain (non---project) install was left as a follow-up, on the theory
that it targets a user-profile location nobody else maintains.

That theory doesn't hold for the three hook writers themselves:
_install_codex_hook, _claude_pretooluse_hooks (claude + codebuddy) and
_gemini_hook all write into project_dir, which defaults to the cwd,
whether or not --project was passed. There is no separate user-profile
destination for these specific files today, so they are always a commit
candidate, and the installing machine's absolute path is wrong in every
other clone regardless of the flag. All three (plus codebuddy, which has
no --project variant at all) now always resolve the bare command; the
`project` parameter threaded through them by Graphify-Labs#3149 is dropped since it no
longer affects the outcome.

`_resolve_graphify_exe`'s absolute-path branch is untouched and still
correct for a genuine, non-shared destination -- if Graphify-Labs#2164 ever gives the
plain install one, that is the point to wire it back in.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

@graphify-labs graphify-labs Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

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.

Formal verification. 2 change(s) alter behavior, breaking input(s) attached.

Behavior changes: \_claude\_pretooluse\_hooks changes behavior, here is the input that shows it.

The verifier found a concrete input on which \_claude\_pretooluse\_hooks behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.

Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.

Evidence: On input \{"strict":"True"\}, the old code produced \[\{'matcher': 'Bash\|Grep', 'hooks': \[\{'type': 'command', 'command': '/opt/venv/bin/graphify hook\-guard search'\}\]\}, \{'matcher': 'Read\|Glob', 'hooks': \[\{'type': 'command', 'command': '/opt/… but the new code produces \[\{'matcher': 'Bash\|Grep', 'hooks': \[\{'type': 'command', 'command': 'graphify hook\-guard search'\}\]\}, \{'matcher': 'Read\|Glob', 'hooks': \[\{'type': 'command', 'command': 'graphify hook\-guar…. Paste that input straight into a regression test.

Behavior changes: \_gemini\_hook changes behavior, here is the input that shows it.

The verifier found a concrete input on which \_gemini\_hook behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.

Guarantee: This is a sound refutation from differential testing (both versions run on many generated inputs): a concrete input on which the two versions provably differ.


Graphify review — findings

Makes agent hook commands always emit the bare graphify invocation instead of a machine-absolute exe path, since .claude/.codebuddy/.gemini/.codex settings files are always written into the working directory and thus always commit candidates — an absolute path resolved on the installing machine would break in every other clone (#3129). Drops the now-dead project parameter threaded through _install_claude_hook, _claude_pretooluse_hooks, _gemini_hook, _install_gemini_hook, _install_codex_hook, _agents_install, and claude_install, hardcoding _resolve_graphify_exe(project=True) at each call site. Adds tests asserting hook commands stay portable, including CodeBuddy which has no project variant at all.

Worth a look

  • resolve_graphify_exe now returns bare name for non-project installs, breaking Scripts/-not-on-PATH hooksgraphify/install.py · Escalate · medium
    • agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
  • Removed public claude_install project keywordgraphify/install.py:1751 · 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 — 503 functions depend on the 286 functions this change touches.

Health — this change adds coupling hotspots:

  • new: dispatch_command() — 2 callers, 123 callees
  • new: codebuddy_install() — 21 callers, 5 callees
  • new: claude_install() — 19 callers, 4 callees
  • new: gemini_install() — 10 callers, 7 callees
  • new: claude_uninstall() — 17 callers, 4 callees
  • new: _project_uninstall() — 5 callers, 13 callees
  • new: dispatch_install_cli() — 2 callers, 31 callees
  • new: codebuddy_uninstall() — 14 callers, 4 callees
  • …and 13 more — each is listed as a finding

Verification — 503 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: 431 function(s) in the blast radius were not formally verified this run

Formal verification

Behavior changes: \_claude\_pretooluse\_hooks changes behavior, here is the input that shows it.

The verifier found a concrete input on which \_claude\_pretooluse\_hooks behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.

Guarantee: This difference was REPRODUCED, the verifier actually ran both versions on that input and saw them disagree. It is real, not an artifact.

Evidence: On input \{"strict":"True"\}, the old code produced \[\{'matcher': 'Bash\|Grep', 'hooks': \[\{'type': 'command', 'command': '/opt/venv/bin/graphify hook\-guard search'\}\]\}, \{'matcher': 'Read\|Glob', 'hooks': \[\{'type': 'command', 'command': '/opt/… but the new code produces \[\{'matcher': 'Bash\|Grep', 'hooks': \[\{'type': 'command', 'command': 'graphify hook\-guard search'\}\]\}, \{'matcher': 'Read\|Glob', 'hooks': \[\{'type': 'command', 'command': 'graphify hook\-guar…. Paste that input straight into a regression test.

Behavior changes: \_gemini\_hook changes behavior, here is the input that shows it.

The verifier found a concrete input on which \_gemini\_hook behaves differently before and after the change. If that change is intended, ship it; if not, this is your bug.

Guarantee: This is a sound refutation from differential testing (both versions run on many generated inputs): a concrete input on which the two versions provably differ.

Could not verify: Could not verify \_agents\_install.

The verifier did not have enough to check \_agents\_install, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: not verifiable: all 200 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly TypeError — names the real obstacle, not a sampling gap)

Could not verify: Could not verify claude\_install.

The verifier did not have enough to check claude\_install, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: not verifiable: all 58 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly TypeError — names the real obstacle, not a sampling gap)

Could not verify: Could not verify gemini\_install.

The verifier did not have enough to check gemini\_install, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: parameter `project_dir` is annotated `Path | None` — outside the synthesizable primitive/collection set

Could not verify: Could not verify \_install\_claude\_hook.

The verifier did not have enough to check \_install\_claude\_hook, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: parameter `project_dir` is annotated `Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify \_install\_codex\_hook.

The verifier did not have enough to check \_install\_codex\_hook, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: parameter `project_dir` is annotated `Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify \_install\_gemini\_hook.

The verifier did not have enough to check \_install\_gemini\_hook, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: parameter `project_dir` is annotated `Path` — outside the synthesizable primitive/collection set

Could not verify: Could not verify \_project\_install.

The verifier did not have enough to check \_project\_install, so it is saying so rather than guessing. No false assurance is the whole point.

Guarantee: No guarantee either way, this is an honest abstention, not a pass.

Note: Reason: parameter `project_dir` is annotated `Path | None` — outside the synthesizable primitive/collection set

· 5 grounded finding(s) anchored inline below; 16 more finding(s) on lines outside this diff (see the check run).

Comment thread graphify/install.py
shutil.copy2(settings_path, backup)
settings_path.write_text(output, encoding="utf-8")
def _install_gemini_hook(project_dir: Path, project: bool = False) -> None:
def _install_gemini_hook(project_dir: Path) -> None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_install_gemini_hook()

high coupling complexity (Ca·Ce = 12).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/install.py
break
return (found or "graphify").replace("\\", "/")
def _install_codex_hook(project_dir: Path, project: bool = False) -> None:
def _install_codex_hook(project_dir: Path) -> None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_install_codex_hook()

high coupling complexity (Ca·Ce = 20).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/install.py
hooks_path.write_text(json.dumps(existing, indent=2), encoding="utf-8")
print(f" .codex/hooks.json -> PreToolUse hook removed")
def _agents_install(project_dir: Path, platform: str, project: bool = False) -> None:
def _agents_install(project_dir: Path, platform: str) -> None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_agents_install()

fans out to 6 callees (efferent coupling); 6 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/install.py
removed = _kilo_uninstall_global()
print("; ".join(removed) if removed else "nothing to remove")
def claude_install(project_dir: Path | None = None, strict: bool = False, project: bool = False) -> None:
def claude_install(project_dir: Path | None = None, strict: bool = False) -> None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regressionclaude_install()

19 callers depend on it (afferent coupling).

Grounded coupling-delta finding (deterministic), not an LLM guess.

Comment thread graphify/install.py
print("Strict mode: the first raw file read per session is blocked until")
print("one `graphify query` runs (toggle with GRAPHIFY_HOOK_STRICT=0).")
def _install_claude_hook(project_dir: Path, strict: bool = False, project: bool = False) -> None:
def _install_claude_hook(project_dir: Path, strict: bool = False) -> None:

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Health regression_install_claude_hook()

high coupling complexity (Ca·Ce = 20).

Grounded coupling-delta finding (deterministic), not an LLM guess.

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