Skip to content

fix(hooks): respect subagent ctx tool availability#834

Open
ken-jo wants to merge 3 commits into
mksglu:nextfrom
ken-jo:fix/subagent-routing-794-832
Open

fix(hooks): respect subagent ctx tool availability#834
ken-jo wants to merge 3 commits into
mksglu:nextfrom
ken-jo:fix/subagent-routing-794-832

Conversation

@ken-jo

@ken-jo ken-jo commented Jun 15, 2026

Copy link
Copy Markdown
Contributor

What / Why / How

Fixes #794.
Fixes #832.

Claude Code subagent-originated WebFetch calls can be blocked by the main-session MCP readiness check even when that subagent cannot invoke any ctx_* MCP tools. In that state the hook denies WebFetch and tells the subagent to call ctx_fetch_and_index, but the suggested tool is not available in the subagent's tool surface.

Claude Code Agent prompt injection also had no supported opt-out. The default injection is useful for subagents that can use context-mode tools, but foreground subagents can hang or become unusable when forced into ctx_* calls that are unavailable or blocking.

With this PR applied, Claude Code PreToolUse distinguishes machine-global MCP readiness from the current caller's ctx_* availability:

  • main-session WebFetch still redirects to ctx_fetch_and_index
  • subagent-originated WebFetch passes through when the hook payload has agent_id or agent_type
  • default Agent prompt injection remains unchanged
  • CONTEXT_MODE_DISABLE_AGENT_INJECTION=1 disables only Agent prompt injection
  • existing subagent invariants are preserved: ToolSearch bootstrap remains present, and <ctx_commands> remains omitted
  • regression coverage is added to the existing tests/hooks/core-routing.test.ts
  • the new env var is documented in README.md

Affected platforms

Agent / CLI scope:

  • Claude Code
  • Cursor
  • VS Code Copilot
  • JetBrains Copilot
  • Gemini CLI
  • Qwen Code
  • OpenCode
  • KiloCode
  • Codex CLI
  • OpenClaw / Pi
  • Kiro
  • Antigravity
  • Zed
  • All standalone MCP server launchers
  • All platforms

OS scope:

  • Linux
  • macOS
  • Windows
  • All operating systems

Scope notes:

  • This is a Claude Code PreToolUse hook-routing change. The agent_id / agent_type discriminator is Claude Code-specific, so other adapters are not changed to infer subagent tool availability.
  • routePreToolUse() keeps mcpToolsAvailable defaulting to true, so existing callers preserve the current redirect behavior unless an adapter explicitly marks the caller as unable to invoke ctx_*.
  • The CONTEXT_MODE_DISABLE_AGENT_INJECTION opt-out affects Agent prompt injection only. It does not disable SessionStart routing guidance, MCP redirects in the main session, or the context-mode MCP server.

Root Cause

Git archaeology:

The failure happens because isMCPReady() is machine-global. It answers whether a context-mode MCP server process is alive on the machine, not whether the current Claude Code caller context can invoke ctx_* tools. Claude Code PreToolUse payloads include agent_id / agent_type for subagent-originated calls, so the adapter can pass that caller context into shared routing.

The PR preserves these older invariants:

Related prior work checked:

Cross-agent / cross-OS risk

This PR is intentionally scoped to Claude Code's hook adapter because agent_id and agent_type are Claude Code PreToolUse payload fields. The shared routing API keeps the previous default for all other adapters, so Cursor, VS Code Copilot, JetBrains Copilot, Gemini CLI, Qwen Code, OpenCode/Kilo, Codex CLI, OpenClaw/Pi, Kiro, Antigravity, Zed, and standalone MCP launchers do not get new subagent tool-availability behavior unless their adapter explicitly opts in later.

The runtime code is plain Node.js hook code and was validated on Linux, macOS, and Windows. No platform-specific path handling, shell parsing, or generated server bundle path is changed.

Validation

Detailed maintainer-style validation results are reported in a PR comment rather than kept in the body.

@ken-jo

ken-jo commented Jun 15, 2026

Copy link
Copy Markdown
Contributor Author

Maintainer-style validation report for the final PR head e3ac14d.

I validated this as a core hook-routing change, not only as a build/typecheck check.

Claim / behavior checked

This PR is intended to fix two Claude Code hook-routing behaviors:

The important invariant is that this must not weaken the normal main-session protection:

Git archaeology / root cause

I checked the relevant history before accepting the fix shape:

The root cause is that isMCPReady() is machine-global. It proves that a context-mode MCP server exists on the machine, but it does not prove that the current Claude Code caller context can invoke ctx_* tools. Claude Code PreToolUse payloads expose subagent-originated calls through agent_id / agent_type, so this PR passes caller-context availability into shared routing without changing the default for other adapters.

Before / after behavior

Using an upstream/next comparison harness:

Before this PR:

  • main WebFetch: denied and redirected to ctx_fetch_and_index
  • subagent WebFetch: also denied and redirected to ctx_fetch_and_index, even though the subagent tool surface cannot call that tool
  • default Agent: modified with the context-mode routing block, ToolSearch bootstrap present, <ctx_commands> omitted
  • CONTEXT_MODE_DISABLE_AGENT_INJECTION=1: no effect

After this PR:

  • main WebFetch: still denied and redirected to ctx_fetch_and_index
  • subagent WebFetch: passes through when agent_id / agent_type is present
  • default Agent: still modified with the context-mode routing block
  • ToolSearch bootstrap remains present for Claude Code
  • <ctx_commands> remains omitted from subagent routing
  • CONTEXT_MODE_DISABLE_AGENT_INJECTION=1: Agent passes through

Regression coverage

Coverage was integrated into the existing owner file tests/hooks/core-routing.test.ts; no new test file was added.

The final test coverage asserts:

  • subagent WebFetch passes through when mcpToolsAvailable: false
  • omitted options preserve the existing default redirect behavior
  • a real hooks/pretooluse.mjs subprocess treats Claude Code subagent hook payloads as ctx_* unavailable
  • MCP-backed redirects for curl, inline HTTP, build tools, and WebFetch pass through only when the caller context cannot invoke ctx_*
  • default Claude Code Agent injection still adds <context_window_protection> and ToolSearch bootstrap
  • default Claude Code Agent injection still omits <ctx_commands>
  • CONTEXT_MODE_DISABLE_AGENT_INJECTION=1 only disables Agent prompt injection

I also ran an independent Claude Haiku adversarial review against the final diff. It found no blockers. The suggested extra tests were the default Agent injection check, WebFetch in the MCP-backed redirect batch, and omitted-options default behavior; those were added in e3ac14d.

3OS validation

Final commit under test: e3ac14d.

Linux local:

  • Node v24.14.0
  • Claude Code 2.1.177
  • npm run typecheck: passed
  • npm run build: passed, including assert-bundle and assert-asymmetric-drift
  • npx vitest run tests/hooks/core-routing.test.ts tests/core/routing.test.ts --reporter=dot: passed, 120 tests
  • direct hooks/pretooluse.mjs stdin/stdout smoke: passed
  • Claude Code Haiku MCP smoke: ctx_execute returned cm794832-agent-smoke-linux-final

macOS via ssh my-mac:

  • Node v26.0.0
  • Claude Code 2.1.177
  • npm run typecheck: passed
  • npm run build: passed, including assert-bundle and assert-asymmetric-drift
  • npx vitest run tests/hooks/core-routing.test.ts tests/core/routing.test.ts --reporter=dot: passed, 120 tests
  • direct hooks/pretooluse.mjs stdin/stdout smoke: passed
  • Claude Code Haiku MCP smoke: ctx_execute returned cm794832-agent-smoke-mac-final

Windows via ssh my-window:

  • Node v24.13.0
  • Claude Code 2.1.177
  • npm run typecheck: passed
  • npm run build: passed, including assert-bundle and assert-asymmetric-drift
  • npx vitest run tests/hooks/core-routing.test.ts tests/core/routing.test.ts --reporter=dot: passed, 120 tests
  • direct hooks/pretooluse.mjs stdin/stdout smoke: passed
  • Claude Code Haiku MCP smoke: ctx_execute returned cm794832-agent-smoke-windows-final

The direct hook smoke on all three OSes verified the actual hook process contract, not only routePreToolUse() unit calls:

  • main WebFetch denied and mentioned ctx_fetch_and_index
  • subagent WebFetch passed through with no hook output
  • default Agent prompt was modified
  • default Agent prompt included <context_window_protection>
  • default Agent prompt included the ToolSearch bootstrap
  • default Agent prompt did not include <ctx_commands>
  • CONTEXT_MODE_DISABLE_AGENT_INJECTION=1 made Agent pass through

Broad-suite note

I also ran the broader local Vitest suite. It did not pass cleanly, but the failures were separated from this PR:

  • local broad run: 7 failed / 4271 passed / 38 skipped
  • the failures were in tests/executor.test.ts, tests/lifecycle.test.ts, tests/adapters/detect-claude-code-in-vscode.test.ts, and tests/util/postinstall-heal.test.ts
  • the same adapter/postinstall failures reproduce on clean upstream/next
  • the lifecycle SIGTERM failure passed when isolated on both this PR and upstream/next, so it appears order/environment-sensitive rather than caused by this routing change
  • none of the broad-suite failures touch the files changed by this PR: README.md, hooks/core/routing.mjs, hooks/pretooluse.mjs, tests/hooks/core-routing.test.ts

I did not treat the broad-suite failure as a pass, but I also do not see evidence that those failures are introduced by this PR.

Conclusion

The #794 / #832 behavior is confirmed, the root cause is history-backed, and the final patch preserves the older #683, #725, and #233 invariants. I consider this merge-worthy after the final e3ac14d validation above.

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