Skip to content

fix: hooks gate on infigraph-mcp liveness before pointing at unreachable tools#35

Open
pradeepmouli wants to merge 2 commits into
intuit:mainfrom
pradeepmouli:fix/hook-mcp-liveness-check
Open

fix: hooks gate on infigraph-mcp liveness before pointing at unreachable tools#35
pradeepmouli wants to merge 2 commits into
intuit:mainfrom
pradeepmouli:fix/hook-mcp-liveness-check

Conversation

@pradeepmouli

Copy link
Copy Markdown
Contributor

Summary

Three of the shipped Claude Code hooks unconditionally instruct the agent to call an mcp__infigraph__* tool, with no fallback if that tool happens to be unreachable (MCP disconnected — e.g. via the worker's own idle-self-termination, I-5/R2.2.3):

  • ENFORCE_HOOK_SCRIPT denies raw Grep/Glob/Bash/Read/Write/Agent calls and points at an MCP tool as the required alternative. Its only escape hatch (the search-fallback sentinel) can never fire in this case, since it's only ever set after a real tool call has already succeeded once — meaning a disconnected MCP server blocks every raw tool forever with no path out at all.
  • SESSION_SAVE_HOOK_SCRIPT unconditionally tells the agent to call save_session, causing full narrative compositions to be discarded on calls that were guaranteed to fail.
  • SESSION_START_HOOK_SCRIPT's compact/clear-recovery paths have the same problem for both save_session and get_latest_session.

A hook script can't query Claude Code's actual MCP connection state (not exposed to hooks), so pgrep -f infigraph-mcp is used as a cheap, necessarily one-sided heuristic: no process found is high-confidence the tool call would fail, but a process found is not proof it's reachable from this specific session (could be stale, orphaned, or serving a different session) — pgrep can only rule the tool out, never confirm it's in.

Given that asymmetry:

  • The two write-side hooks (session-save, session-start) keep pgrep as a framing hint only — the actual gate against wasted composition is always a cheap availability check done before writing any content, regardless of what pgrep found, with an embedded fallback recipe describing the exact on-disk SessionStore schema so the write survives even with MCP down.
  • The enforcement hook's "no process at all" case is a real bypass instead (exit 0), since blocking with zero recovery path is worse than occasionally allowing a raw tool when it wasn't strictly necessary. Every denial reason also now tells the agent how to self-serve the existing sentinel if the suggested tool turns out unreachable despite a process being found, so retrying doesn't just hit the same block again.

get_latest_session gets no fallback recipe at all — unlike a write, a read needs no new content composed, and the hook already reads .infigraph/sessions/*.json directly for its own startup/resume context injection, so that's surfaced up front regardless of tool reachability.

Scope

Deliberately narrow — only these three hooks. The remaining shipped hooks either can't fire while MCP is down at all (PostToolUse hooks gated on a specific MCP tool call having already succeeded: SESSION_RESET, TEST_CONTEXT_SENTINEL, SEARCH_FALLBACK_SENTINEL), or have no MCP dependency to begin with (SESSION_END_SAVE is already transcript/file-based, EDIT_TRACKER, CLEAR_SUGGEST).

Test plan

  • cargo test -p infigraph-cli hooks:: — 14/14 pass unchanged (existing tests assert on stable substrings, e.g. "deny-by-default", "Grep)", none of which moved)
  • cargo fmt -p infigraph-cli -- --check clean
  • cargo clippy -p infigraph-cli --all-targets -- -D warnings clean
  • Every modified constant round-tripped through real bash -n syntax verification after extraction
  • Manually verified both branches of the pgrep gate in all three scripts (process present / mocked absent) produce valid JSON with the expected content
  • Verified the .search-fallback-allowed sentinel is a genuine global bypass across every tool type ENFORCE_HOOK_SCRIPT covers (not scoped to search specifically), confirming the self-serve recovery instruction is mechanically correct
  • Full pre-commit hook (fmt, clippy, all three --ignored perf suites) ran clean on the actual commit, not bypassed

🤖 Generated with Claude Code

https://claude.ai/code/session_01TAXDJyFdnA4BV1U2fxufdC

…ble tools

Three shipped hooks unconditionally instruct Claude to call an
mcp__infigraph__* tool with no fallback if that tool is unreachable
(MCP disconnected, e.g. via the worker's own idle-self-termination):

- The enforcement hook denies raw Grep/Glob/Bash/Read/Write/Agent calls
  and points at an MCP tool as the required alternative. Its only escape
  hatch (the search-fallback sentinel) can never fire in this case, since
  it's only ever set AFTER a real tool call has already succeeded once --
  meaning a disconnected MCP server blocks every raw tool forever with no
  path out at all.
- The session-save reminder unconditionally tells Claude to call
  save_session, causing full narrative compositions to be wasted on
  calls that were guaranteed to fail.
- The session-start hook's compaction/clear-recovery paths have the same
  problem for both save_session and get_latest_session.

A hook script can't query Claude Code's actual MCP connection state (not
exposed to hooks), so `pgrep -f infigraph-mcp` is used as a cheap,
necessarily one-sided heuristic: no process found is high-confidence the
tool call would fail, but a process found is NOT proof it's reachable
from this session (could be stale, orphaned, or serving a different
session) -- pgrep can only rule the tool OUT, never confirm it's IN.

Given that asymmetry, the two write-side hooks (session-save,
session-start) keep pgrep as a framing hint only -- the actual gate
against wasted composition is always a cheap availability check done
before writing any content, regardless of what pgrep found, with an
embedded fallback recipe describing the exact on-disk SessionStore
schema so the write survives even with MCP down. The enforcement hook's
"no process at all" case is a real bypass instead (exit 0), since
blocking with no recovery path is worse than occasionally allowing a raw
tool when it wasn't strictly necessary; every denial reason also now
tells Claude how to self-serve the existing sentinel if the suggested
tool turns out unreachable despite a process being found, so retrying
doesn't just hit the same block again.

get_latest_session gets no fallback recipe at all -- unlike a write, a
read needs no new content composed, and the hook already reads
.infigraph/sessions/*.json directly for its own startup/resume context
injection, so that's surfaced up front regardless of tool reachability.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TAXDJyFdnA4BV1U2fxufdC
…s counter

SendMessage-delivered turns (idle notifications, status pings from
background agents) fire UserPromptSubmit just like real human input, so
concurrent subagents were inflating the save-reminder counter far faster
than actual user exchanges -- one live session saw 183 fires from ~20
real turns. Skip the counter entirely when the prompt is a
<teammate-message> relay, so it tracks what its own doc comment already
claims: user exchanges, not agent-orchestration chatter.
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