Skip to content

mcp_hooks is unreachable for custom harnesses — MCP_HOOK_SERVERS reaches exactly one runtime #5571

Description

@jimparkins

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:
      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<String> 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 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions