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
-
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).
-
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.
-
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.
Motivation
MCP_HOOK_SERVERSis documented as a portable convention —docs/MCP_DRIVEN_HOOKS.md:106: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
mainatbe48ce98bd163899197b79a82ad5b2bcf0bc9b54.The three facts
mcp_hooksis a field on the static built-in catalog only —KnownAcpRuntime.mcp_hooks(desktop/src-tauri/src/managed_agents/discovery/runtime_metadata.rs:12),trueforbuzz-agent(discovery.rs:188),falsefor goose, claude andcodex (
:86,:121,:154).Both gates that act on it resolve through that catalog and nothing else.
managed_agents/runtime.rs:546-549:commands/agents_deploy.rs:60-73:policy_envgetsMCP_HOOK_SERVERS=*only insideif let Some(runtime) = runtime.known_acp_runtime(discovery.rs:268-279) searchesKNOWN_ACP_RUNTIMESbyid, command basename or alias. A custom harness id is not in that array, so
it resolves to
None, sois_some_andisfalse, on both paths.HarnessDefinition— the user-authored custom-harness schema,managed_agents/custom_harnesses.rs:49-70— carriesid,label,command,args,env,installInstructionsUrl,installHint. There isno 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 executablebasename, 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):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.
mcpHooksis not on that side of the line. It grants no new code execution andnames no new resource: it enables the
_Stop/_PostCompacttool callsagainst 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: falsefor goose, claude and codex is not a gap and I amnot 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"). Thebuilt-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(defaultfalse) toHarnessDefinition, andhave both gates read the effective harness descriptor rather than
known_acp_runtimedirectly.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>toHarnessDefinitionand threads it throughsave_custom_harness→ the effective harness descriptor → spawn and restarthashing.
mcpHooksis a sibling of that field on the same path. If #4078 landsfirst, this becomes a small follow-up rather than new plumbing.
Alternatives considered
the MCP server side, not the harness side, and the harness's willingness to
call
_Stopis not observable before a turn ends.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.
MCP_HOOK_SERVERSfromHarnessDefinition.env. This doeswork 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_COMMANDto run several MCP servers",open since 2026-07-27) changes
build_mcp_serversincrates/buzz-acp; ittouches neither
mcp_hooksnorHarnessDefinition. It lifts the otherconstraint (#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-agentonly, one server. Reach with #3196 merged:buzz-agentonly, many servers.Additional context
Searched issues and PRs before filing. Closest existing work: #4078 (the
mcpCommandprecedent above), #3196 / #2899 (server count, not hookenablement), #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_hookson a custom harness.Found while building an out-of-tree
buzz-backend-*provider that runs Buzzagents in Orca worktrees, shipping an MCP hook server
alongside a persona pack. The pack had to pin
runtime: buzz-agentand add atest 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.