fix(init): set ENABLE_TOOL_SEARCH=true so Claude Code keeps deferring tools (#746) (#995)

## Description

Claude Code disables on-demand tool loading (Tool Search) when
`ANTHROPIC_BASE_URL` is a custom host and `ENABLE_TOOL_SEARCH` is unset,
materializing all MCP/system tool schemas into its context window
(#746). With many MCP servers this overflows the window — breaking
sub-agent spawns ("prompt too long, ~214k > 200k") and forcing constant
compaction. `headroom wrap claude` already sets it; `init`/install did
not. Refs #746.

## Type of Change

- [x] Bug fix (non-breaking change that fixes an issue)

## Changes Made

- Keep tool deferral on at both entry points, sharing one
`TOOL_SEARCH_ENV` / `TOOL_SEARCH_DEFAULT` constant from the Claude
provider package (`providers/claude/runtime.py`) so the key/default
can't drift:
- `init` (`_ensure_claude_hooks`): sets `ENABLE_TOOL_SEARCH=true` via
`setdefault`, respecting a pre-existing user-provided value.
- `install` (`build_install_env`): always writes
`ENABLE_TOOL_SEARCH=true`. This is the headroom-managed install env
(recorded and reverted on uninstall), so it is authoritative rather than
deferring to an existing value.

## Testing

- [x] Unit tests pass (`pytest`)
- [x] New tests added for new functionality
- [x] Manual testing performed

### Test Output

```text
$ pytest tests/test_cli/test_init_enable_tool_search.py -q
3 passed in 0.63s
```

## Real Behavior Proof

- Environment: Python 3.14, headroom proxy on :8799, ~19 MCP servers
connected
- Exact command / steps: launched `claude` through the proxy with vs
without `ENABLE_TOOL_SEARCH=true`, asked each to spawn 5 parallel
sub-agents
- Observed result: without it, all 5 sub-agents fail ("prompt too long,
~214k > 200k"); with `ENABLE_TOOL_SEARCH=true` all 5 succeed and traffic
compresses
- Not tested: non-Claude-Code agents

## Review Readiness

- [x] I have performed a self-review
- [x] This PR is ready for human review

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Eyal Mizrachi 2026-06-19 12:26:26 -04:00 committed by GitHub
parent f03e77bec0
commit 500ec2b7fa
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 102 additions and 11 deletions

View file

@ -424,7 +424,11 @@ def test_ensure_claude_hooks_rewrites_existing_entries(monkeypatch, tmp_path: Pa
init_cli._ensure_claude_hooks(settings_path, "init-local-demo", 9001)
payload = json.loads(settings_path.read_text(encoding="utf-8"))
assert payload["env"] == {"KEEP": "1", "ANTHROPIC_BASE_URL": "http://127.0.0.1:9001"}
assert payload["env"] == {
"KEEP": "1",
"ANTHROPIC_BASE_URL": "http://127.0.0.1:9001",
"ENABLE_TOOL_SEARCH": "true",
}
session_entries = payload["hooks"]["SessionStart"]
assert session_entries[0] == "not-a-dict"
assert session_entries[1] == {"hooks": "not-a-list"}

View file

@ -0,0 +1,42 @@
"""`headroom init claude` must set ENABLE_TOOL_SEARCH (GH #746).
Claude Code disables on-demand tool loading when ANTHROPIC_BASE_URL is a custom
host and ENABLE_TOOL_SEARCH is unset, materializing all MCP/system tool schemas
into the context window which breaks sub-agent spawns and forces compaction.
`headroom wrap claude` already sets it; `init` (and the persistent install) must
too, otherwise init-wired users silently get eager tools.
"""
from __future__ import annotations
import json
from pathlib import Path
from headroom.cli import init as init_cli
from headroom.providers.claude import install as claude_install
def test_ensure_claude_hooks_sets_enable_tool_search(tmp_path: Path) -> None:
settings = tmp_path / "settings.json"
init_cli._ensure_claude_hooks(settings, profile="init-user", port=8787)
env = json.loads(settings.read_text(encoding="utf-8"))["env"]
assert env["ANTHROPIC_BASE_URL"] == "http://127.0.0.1:8787"
assert env["ENABLE_TOOL_SEARCH"] == "true"
def test_ensure_claude_hooks_respects_user_tool_search_value(tmp_path: Path) -> None:
settings = tmp_path / "settings.json"
settings.write_text(
json.dumps({"env": {"ENABLE_TOOL_SEARCH": "auto"}}) + "\n", encoding="utf-8"
)
init_cli._ensure_claude_hooks(settings, profile="init-user", port=8787)
env = json.loads(settings.read_text(encoding="utf-8"))["env"]
assert env["ENABLE_TOOL_SEARCH"] == "auto" # setdefault preserves the user's choice
def test_build_install_env_includes_enable_tool_search() -> None:
env = claude_install.build_install_env(port=8787, backend="anthropic")
assert env["ENABLE_TOOL_SEARCH"] == "true"
assert env["ANTHROPIC_BASE_URL"].endswith(":8787")