fix(hooks): make graph guidance executable and strict per agent - #3281
fix(hooks): make graph guidance executable and strict per agent#3281logan683 wants to merge 8 commits into
Conversation
…se hooks
The generated hook resolved its command via shutil.which("graphify"), which
returns the console-script shim. For uv-tool and pipx installs that shim is
unsigned; on Windows with Smart App Control enabled, code-integrity policy
refuses to load it, so the hook can never run. Both ~/.local/bin/graphify.exe
and the venv Scripts/graphify.exe are NotSigned, so resolving to a different
shim directory does not help.
The hook also had no failure guard. A PreToolUse hook that exits non-zero
blocks the tool call it was meant to advise, so an advisory graph check could
break the host on any launcher failure.
Resolve to '<sys.executable> -m graphify' via the existing hooks._pinned_python(),
the same reasoning already applied to git hooks, and make both the POSIX and
Windows commands fail open with a timeout.
The Graphify-Labs#2165 regression parses the subcommand out of the hook command; it now
handles both supported grammars rather than assuming the subcommand is the
second token, so it keeps proving the same contract.
Refs Graphify-Labs#3280
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.
Formal verification. No changes could be formally verified in this run.
Graphify review — findings
Resolves the Codex PreToolUse hook command through the running interpreter as <pinned-python> -m graphify instead of shutil.which("graphify"), so hardened Windows (Smart App Control) that refuses to load the unsigned console shim can still run the hook. Makes the generated hook fail open by appending || true to the POSIX command and & exit /b 0 to a new commandWindows variant (plus a 10s timeout), so an advisory graph check that errors never blocks the host's tool call. Updates the CLI-subcommand assertion to parse both the direct-launcher and -m graphify command shapes and to ignore the fail-open guard.
Worth a look
- Pinned interpreter path is inserted unquoted into shell hook —
graphify/install.py:1455· Escalate · medium- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Pinned Python path is embedded unquoted in generated Codex hook commands —
graphify/install.py:1455· 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 — 505 functions depend on the 288 functions this change touches.
Health — this change adds coupling hotspots:
- new:
dispatch_command()— 2 callers, 123 callees - new:
codebuddy_install()— 20 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 — 505 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: 433 function(s) in the blast radius were not formally verified this run
Formal verification
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 \_resolve\_graphify\_exe.
The verifier did not have enough to check \_resolve\_graphify\_exe, 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: non-vacuity: domain too small (only 1 distinct inputs exercised, need 2) — 'no divergence' would be near-vacuous
· 21 more finding(s) on lines outside this diff (see the check run).
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 5 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","project":"True"\}, the old code produced \[\{'matcher': 'Bash\|Grep', 'hooks': \[\{'type': 'command', 'command': 'graphify hook\-guard search'\}\]\}, \{'matcher': 'Read\|Glob', 'hooks': \[\{'type': 'command', 'command': 'graphify hook\-guar… but the new code produces \[\{'matcher': 'Bash\|Grep', 'hooks': \[\{'type': 'command', 'command': '"$\(cat graphify\-out/\.graphify\_python\)" \-m graphify hook\-guard search \|\| true', 'commandWindows': 'powershell\.exe \-N…. Paste that input straight into a regression test.
Behavior changes: \_resolve\_graphify\_exe changes behavior, here is the input that shows it.
The verifier found a concrete input on which \_resolve\_graphify\_exe 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 \{"project":"True"\}, the old code produced 'graphify' but the new code produces '"$\(cat graphify\-out/\.graphify\_python\)" \-m graphify'. Paste that input straight into a regression test.
Graphify review — findings
Reroutes all generated agent guidance to prefer the MCP query_graph tool, falling back to the interpreter recorded in graphify-out/.graphify_python rather than a bare graphify/graphify.exe launcher, so hardened Windows and Application Control hosts don't route agents through an unsigned shim. Fixes strict Claude hooks to key deny markers by session_id plus agent_id instead of a single graph-wide 30-minute stamp, so a parent's first read no longer consumes every subagent's guard. Makes project-scoped hooks resolve Graphify through each clone's .graphify_python sidecar and fail open (no graph decision) until that sidecar exists, and moves the strict-mode override to GRAPHIFY_HOOK_STRICT in the Claude process environment.
Worth a look
- Project hook executes interpreter path from repository file —
graphify/install.py:1443· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- User-controlled query text is placed in a shell command with command substitution enabled —
graphify/skills/amp/references/query.md:68· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Repository-controlled interpreter path is executed for graph queries —
graphify/skills/claw/references/query.md:65· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Query text is interpolated into a shell command —
graphify/skills/copilot/references/query.md:65· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Repo-controlled interpreter path executed from VSCode instructions —
graphify/always_on/vscode-instructions.md:5· 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 — 2223 functions depend on the 2027 functions this change touches.
Health — this change adds coupling hotspots:
- new:
dispatch_command()— 2 callers, 122 callees - new:
codebuddy_install()— 20 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:
render()— 13 callers, 5 callees - new:
dispatch_install_cli()— 2 callers, 31 callees - …and 20 more — each is listed as a finding
Verification — 2223 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: 2169 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","project":"True"\}, the old code produced \[\{'matcher': 'Bash\|Grep', 'hooks': \[\{'type': 'command', 'command': 'graphify hook\-guard search'\}\]\}, \{'matcher': 'Read\|Glob', 'hooks': \[\{'type': 'command', 'command': 'graphify hook\-guar… but the new code produces \[\{'matcher': 'Bash\|Grep', 'hooks': \[\{'type': 'command', 'command': '"$\(cat graphify\-out/\.graphify\_python\)" \-m graphify hook\-guard search \|\| true', 'commandWindows': 'powershell\.exe \-N…. Paste that input straight into a regression test.
Behavior changes: \_resolve\_graphify\_exe changes behavior, here is the input that shows it.
The verifier found a concrete input on which \_resolve\_graphify\_exe 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 \{"project":"True"\}, the old code produced 'graphify' but the new code produces '"$\(cat graphify\-out/\.graphify\_python\)" \-m graphify'. Paste that input straight into a regression test.
Could not verify: Could not verify dispatch\_command.
The verifier did not have enough to check dispatch\_command, 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 23 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly SystemExit — names the real obstacle, not a sampling gap)
No difference found (not proven): No behavior difference found in \_mark\_session\_denied (not a proof).
The verifier ran both versions of \_mark\_session\_denied on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
No difference found (not proven): No behavior difference found in \_run\_hook\_guard (not a proof).
The verifier ran both versions of \_run\_hook\_guard on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
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
· 1 grounded finding(s) anchored inline below; 27 more finding(s) on lines outside this diff (see the check run).
| f'& $p -m graphify {args}; exit 0"' | ||
| ) | ||
| return f'cmd /c "{_resolve_graphify_exe()} {args}" & exit /b 0' | ||
| def _install_codex_hook(project_dir: Path, project: bool = False) -> None: |
There was a problem hiding this comment.
_install_codex_hook()
7 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
The generated Windows command may be run by cmd.exe or by PowerShell, and no bare fail-open suffix is correct in both. `& exit /b 0` is cmd syntax; run by PowerShell the bare `&` is the background operator, so the hook spawns a job and exits 1 - the fail-open suffix caused the very failure it was added to prevent (Graphify-Labs#3280). `; exit 0` has the mirror problem: valid in PowerShell, rejected by cmd.exe. Naming the interpreter removes the ambiguity. Measured on Windows, the generated string now exits 0 under both pwsh and cmd.exe; the previous form exited 1 under pwsh. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPaGbwQeyWLwbVm9uRgHtU
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 5 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","project":"True"\}, the old code produced \[\{'matcher': 'Bash\|Grep', 'hooks': \[\{'type': 'command', 'command': 'graphify hook\-guard search'\}\]\}, \{'matcher': 'Read\|Glob', 'hooks': \[\{'type': 'command', 'command': 'graphify hook\-guar… but the new code produces \[\{'matcher': 'Bash\|Grep', 'hooks': \[\{'type': 'command', 'command': '"$\(cat graphify\-out/\.graphify\_python\)" \-m graphify hook\-guard search \|\| true', 'commandWindows': 'powershell\.exe \-N…. Paste that input straight into a regression test.
Behavior changes: \_resolve\_graphify\_exe changes behavior, here is the input that shows it.
The verifier found a concrete input on which \_resolve\_graphify\_exe 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 \{"project":"True"\}, the old code produced 'graphify' but the new code produces '"$\(cat graphify\-out/\.graphify\_python\)" \-m graphify'. Paste that input straight into a regression test.
Graphify review — findings
Routes generated agent guidance to the MCP query_graph tool first and, for CLI fallback, to the graph's recorded interpreter via graphify-out/.graphify_python instead of a bare graphify launcher, so Application Control / hardened Windows hosts don't get pushed through an unsigned graphify.exe shim. Keys strict Claude deny markers by session_id plus agent_id rather than one graph-wide 30-minute query stamp, so a parent's first read no longer consumes every subagent's first-read guard. Resolves project-scoped hooks through each clone's .graphify_python sidecar and fails open (no graph decision) until that sidecar exists, preserving committed-config portability without emitting an unrunnable hook.
Worth a look
- User-controlled query text is interpolated into a shell command —
graphify/skills/claw/references/query.md:65· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Verbatim user question is interpolated into a shell command —
graphify/skills/opencode/references/query.md:170· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Verbatim user question is interpolated into a shell command inside double quotes —
graphify/skills/pi/references/query.md:171· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Skill executes interpreter path read from workspace-controlled file —
graphify/skills/vscode/references/query.md:65· Escalate · high- agreed by 2 of 2 members but NOT verified (no proof, no reproducing execution) — consensus is not a verdict; needs human review
- Strict hook no longer honors recent query/explain/path orientation —
graphify/cli.py:1309· 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 — 2223 functions depend on the 2027 functions this change touches.
Health — this change adds coupling hotspots:
- new:
dispatch_command()— 2 callers, 122 callees - new:
codebuddy_install()— 20 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:
render()— 13 callers, 5 callees - new:
dispatch_install_cli()— 2 callers, 31 callees - …and 20 more — each is listed as a finding
Verification — 2223 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: 2169 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","project":"True"\}, the old code produced \[\{'matcher': 'Bash\|Grep', 'hooks': \[\{'type': 'command', 'command': 'graphify hook\-guard search'\}\]\}, \{'matcher': 'Read\|Glob', 'hooks': \[\{'type': 'command', 'command': 'graphify hook\-guar… but the new code produces \[\{'matcher': 'Bash\|Grep', 'hooks': \[\{'type': 'command', 'command': '"$\(cat graphify\-out/\.graphify\_python\)" \-m graphify hook\-guard search \|\| true', 'commandWindows': 'powershell\.exe \-N…. Paste that input straight into a regression test.
Behavior changes: \_resolve\_graphify\_exe changes behavior, here is the input that shows it.
The verifier found a concrete input on which \_resolve\_graphify\_exe 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 \{"project":"True"\}, the old code produced 'graphify' but the new code produces '"$\(cat graphify\-out/\.graphify\_python\)" \-m graphify'. Paste that input straight into a regression test.
Could not verify: Could not verify dispatch\_command.
The verifier did not have enough to check dispatch\_command, 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 23 sampled inputs raised on both versions — the function never executed, so 'no divergence' would be vacuous (mostly SystemExit — names the real obstacle, not a sampling gap)
No difference found (not proven): No behavior difference found in \_mark\_session\_denied (not a proof).
The verifier ran both versions of \_mark\_session\_denied on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
No difference found (not proven): No behavior difference found in \_run\_hook\_guard (not a proof).
The verifier ran both versions of \_run\_hook\_guard on many inputs and saw identical behavior every time. Strong evidence the change is safe, but evidence, not a proof.
Guarantee: Empirical: differential testing (both versions run on many generated inputs). A divergence on an untested input remains possible, so this is 'no counterexample found', not 'proven equivalent'.
Note: An input the sampler did not try could still differ.
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
· 1 grounded finding(s) anchored inline below; 27 more finding(s) on lines outside this diff (see the check run).
| 'powershell.exe -NoProfile -NonInteractive -Command ' | ||
| f'"{inner}; exit 0"' | ||
| ) | ||
| def _install_codex_hook(project_dir: Path, project: bool = False) -> None: |
There was a problem hiding this comment.
_install_codex_hook()
7 callers depend on it (afferent coupling).
Grounded coupling-delta finding (deterministic), not an LLM guess.
Fixes #3280.
Problem
Generated hooks and agent guidance could route Windows through an unsigned
graphify.exeshim that Application Control refuses to load. Strict Claude enforcement was also weakened by one graph-wide 30-minute query stamp, and a parent session consumed the only deny marker for every subagent sharing itssession_id.Fix
python -m graphify, quote interpreter paths, fail open, and add the Windows command form.graphify-out/.graphify_python.query_graph; use the recorded interpreter for query/path/explain/update fallback guidance across generated host skills and runtime nudges.session_idplusagent_id, hashing long identities to prevent sibling collisions.Evidence
skillgen-checkpasses all 134 generated/expected artifacts.cli.py,install.py, and all four host skill files are SHA-equal to this branch.Limits
.graphify_pythonsidecar fails open until Graphify resolves its environment in that clone.