`mcp_hooks` is unreachable for custom harnesses — MCP_HOOK_SERVERS reaches exactly one runtime
- Dominant language
- Rust
- Stars
- 32.7k
- Forks
- 4.3k
- Avg merge
- 1d 13h
- Merged PRs (30d)
- 253
Description
**Motivation**
`MCP_HOOK_SERVERS` is documented as a portable convention — `docs/MCP_DRIVEN_HOOKS.md:106`:
> `MCP_HOOK_SERVERS` is a standard env var name intended for cross-agent adoption.
A custom ACP harness cannot adopt it. Not "it's awkward" — there is no field to
set and no code path that would read one. The convention's practical reach today
is exactly one runtime: `buzz-agent`.
All references verified against `main` at
`be48ce98bd163899197b79a82ad5b2bcf0bc9b54`.
**The three facts**
1. `mcp_hooks` is a field on the *static built-in* catalog only —
`KnownAcpRuntime.mcp_hooks` (`desktop/src-tauri/src/managed_agents/discovery/runtime_metadata.rs:12`),
`true` for `buzz-agent` (`discovery.rs:188`), `false` for goose, claude and
codex (`:86`, `:121`, `:154`).
2. Both gates that act on it resolve through that catalog and nothing else.
- Local spawn, `managed_agents/runtime.rs:546-549`:
```rust
let runtime_meta = known_acp_runtime(effective_command);
if runtime_meta.is_some_and(|r| r.mcp_hooks) {
command.env("MCP_HOOK_SERVERS", "*");
}
```
- Provider deploy, `commands/agents_deploy.rs:60-73`: `policy_env` gets
`MCP_HOOK_SERVERS=*` only inside `if let Some(runtime) = runtime`.
`known_acp_runtime` (`discovery.rs:268-279`) searches `KNOWN_ACP_RUNTIMES` by
id, command basename or alias. A custom harness id is not in that array, so
it resolves to `None`, so `is_some_and` is `false`, on both paths.
3. `HarnessDefinition` — the user-authored custom-harness schema,
`managed_agents/custom_harnesses.rs:49-70` — carries `id`, `label`,
`command`, `args`, `env`, `installInstructionsUrl`, `installHint`. There is
no hooks field, so even if a gate wanted to consult the definition there
would be nothing to consult.
A corollary that shows the shape of the problem: because
`normalize_command_identity` (`discovery.rs:239-259`) matches on the executable
basename, the *only* way a custom harness gets hook tools today is to name its
binary `buzz-agent`. Capability is being inferred from a filename.
**Why I think this is collateral rather than intent**
The schema's own doc comment states a real security line
(`custom_harnesses.rs:44-48`):
> Only the fields a custom harness definition is permitted to carry are
> included here — install commands and avatar URLs are intentionally absent
> (security line: no remote icon URLs from user-editable config).
That line is correct and I am not asking to cross it. User-editable icon URLs
plus user-editable install commands is a phishing kit, and a JSON file dropped
into a directory is the wrong place for either.
`mcpHooks` is not on that side of the line. It grants no new code execution and
names no new resource: it enables the `_Stop` / `_PostCompact` tool calls
against MCP servers the same user already configured, and hook behaviour is
already bounded independently of who enabled it — 2.5s per-hook timeout,
3 objections per prompt, off by default (`docs/MCP_DRIVEN_HOOKS.md`, Configuration).
It is a one-bit declaration of *"my harness implements this protocol"*, which is
a fact only the harness author knows.
Equally, `mcp_hooks: false` for goose, claude and codex is not a gap and I am
not asking to flip it — those runtimes genuinely do not implement the hook
tools, and the catalog is telling the truth about them
(`runtime/tests.rs:102`: `"codex-acp does not handle MCP_HOOK_SERVERS"`). The
built-in table can be accurate because Buzz owns those entries. For a custom
harness Buzz cannot know, and currently provides no way to be told.
**Proposed solution**
Add an optional `mcpHooks: bool` (default `false`) to `HarnessDefinition`, and
have both gates read the effective harness descriptor rather than
`known_acp_runtime` directly.
The precedent already exists and is one field wide: **PR #4078** ("feat(desktop):
support MCP sidecars for custom harnesses", open) adds an optional
`mcp_command: Option` to `HarnessDefinition` and threads it through
`save_custom_harness` → the effective harness descriptor → spawn and restart
hashing. `mcpHooks` is a sibling of that field on the same path. If #4078 lands
first, this becomes a small follow-up rather than new plumbing.
**Alternatives considered**
- *Infer it.* There is nothing to infer from — the hook tools are discovered on
the MCP server side, not the harness side, and the harness's willingness to
call `_Stop` is not observable before a turn ends.
- *A user-level "enable hooks" toggle on the agent instead of the harness
definition.* Wrong altitude: hook support is a property of the harness binary,
not of one agent that uses it, and it would then need setting per agent.
- *Leave it and set `MCP_HOOK_SERVERS` from `HarnessDefinition.env`.* This does
work today for the local-spawn path and is what an out-of-tree provider ends
up doing on the deploy path. It is a workaround, not the contract: it puts a
reserved-namespace control-plane variable into a user env map, and nothing
validates or reports it.
**Does PR #3196 change the picture?**
No — checked, and worth stating because the two limits are easy to conflate.
**#3196** ("feat(acp): allow `BUZZ_ACP_MCP_COMMAND` to run several MCP servers",
open since 2026-07-27) changes `build_mcp_servers` in `crates/buzz-acp`; it
touches neither `mcp_hooks` nor `HarnessDefinition`. It lifts the *other*
constraint (#2899, one MCP server per agent), which makes hooks considerably
more useful once they are reachable — a hook server and a tool server can
finally coexist — but it does not make them reachable.
Reach today: `buzz-agent` only, one server. Reach with #3196 merged:
`buzz-agent` only, many servers.
**Additional context**
Searched issues and PRs before filing. Closest existing work: **#4078** (the
`mcpCommand` precedent above), **#3196** / **#2899** (server count, not hook
enablement), **#4550** (per-agent MCP injection for preset/custom runtimes),
**#3385** (BYOH harness receives no authenticated reply tool — #4078 closes it),
**#3780** (portable agent-session hook *semantics*, a layer above this).
Nothing found that asks for `mcp_hooks` on a custom harness.
Found while building an out-of-tree `buzz-backend-*` provider that runs Buzz
agents in [Orca](https://stably.ai) worktrees, shipping an MCP hook server
alongside a persona pack. The pack had to pin `runtime: buzz-agent` and add a
test that fails if anyone changes it, because the persona format can express a
runtime whose hooks will be silently discarded and cannot express whether they
will be. Buzz Desktop 0.5.8, macOS.
Contributor guide
Research direction
Start with HarnessDefinition in managed_agents/custom_harnesses.rs and compare the mcp_command plumbing from PR #4078. Trace the effective harness descriptor into managed_agents/runtime.rs and commands/agents_deploy.rs, then inspect the built-in lookup in discovery.rs. Done means custom harnesses can explicitly declare hook support on both paths while the default remains disabled and built-in runtime behavior is unchanged.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- rust
- Domain
- desktop-dev, devtools
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 55/100