Skip to content

fix(hooks): make graph guidance executable and strict per agent - #3281

Open
logan683 wants to merge 8 commits into
Graphify-Labs:v8from
logan683:pr/hook-fail-open
Open

fix(hooks): make graph guidance executable and strict per agent#3281
logan683 wants to merge 8 commits into
Graphify-Labs:v8from
logan683:pr/hook-fail-open

Conversation

@logan683

@logan683 logan683 commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

Fixes #3280.

Problem

Generated hooks and agent guidance could route Windows through an unsigned graphify.exe shim 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 its session_id.

Fix

  • Invoke generated hooks through the running interpreter as python -m graphify, quote interpreter paths, fail open, and add the Windows command form.
  • Keep committed project hooks portable by reading each clone's graphify-out/.graphify_python.
  • Prefer MCP query_graph; use the recorded interpreter for query/path/explain/update fallback guidance across generated host skills and runtime nudges.
  • Remove the graph-wide query stamp from strict gating.
  • Key deny markers by session_id plus agent_id, hashing long identities to prevent sibling collisions.
  • Preserve always-on generator byte integrity through explicit sanctioned substitutions.

Evidence

  • Focused RED/GREEN checks cover the observed launcher, fail-open, shared-stamp, subagent, quoting, generated-guidance, and settings-merge failures.
  • skillgen-check passes all 134 generated/expected artifacts.
  • GitHub CI passes Python 3.10, Python 3.12, security scan, and skillgen.
  • Fresh live MCP retrieval passes on Claude, Codex, Agy (GPT-OSS and Claude Sonnet), and OpenCode.
  • Installed cli.py, install.py, and all four host skill files are SHA-equal to this branch.

Limits

  • Existing client sessions whose MCP child was stopped must restart/reconnect; fresh sessions attach successfully.
  • A missing project .graphify_python sidecar fails open until Graphify resolves its environment in that clone.

…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

@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. 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 hookgraphify/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 commandsgraphify/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).

@logan683 logan683 changed the title fix(install): pin the interpreter and fail open in generated PreToolUse hooks fix(hooks): make graph guidance executable and strict per agent Sep 2, 2026

@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 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 filegraphify/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 enabledgraphify/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 queriesgraphify/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 commandgraphify/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 instructionsgraphify/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).

Comment thread graphify/install.py
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:

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()

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

@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 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 commandgraphify/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 commandgraphify/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 quotesgraphify/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 filegraphify/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 orientationgraphify/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).

Comment thread graphify/install.py
'powershell.exe -NoProfile -NonInteractive -Command '
f'"{inner}; exit 0"'
)
def _install_codex_hook(project_dir: Path, project: 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_codex_hook()

7 callers depend on it (afferent coupling).

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.

install: generated PreToolUse hook has no fail-open guard and resolves to an unrunnable shim (blocks every tool call)

1 participant