55. The Implicit Claude Default Is Never Reached Without Approval¶
Date: 2026-10-13
Status: Accepted
Context¶
Every config has a built-in claude (Claude CLI) entry, and escalation_agent defaults to it. That default was used silently: when the primary agent was unreachable resolveTarget routed every turn to it with only a startup warning, and /escalate, the router and the model's own escalate() all landed on it. For a user who never configured an escalation agent — typically running a local or private model on purpose — that sends the prompt and the conversation context to a cloud model they did not choose.
Decision¶
- "Defined" means configured.
Config.EscalationAgentIsImplicit()is true whenescalation_agentis unset and noclaudeentry exists inagents, or when it names an agent that does not exist.escalation_agent: "claude"(or aclaudeentry) is a deliberate choice and keeps the automatic behavior, as does any user-named escalation agent. - Every route to the implicit default asks first. A turn headed there — a down primary falling back, an explicit
/escalateor--escalate(an instruction to escalate is not a choice of Claude), a pin, sticky escalation, a router decision — setsroutedTurn.NeedsClaudeConsent, and so does the model's ownescalate()handoff (a hook on the turn context,confirmSelfEscalation, because single-prompt mode deliberately has nodispatchAgents). The question names the reason: primary unavailable, no escalation agent configured, or the model's escalation reason. - Default No, once per session. The prompt is
[y/N](events.PermissionRequest.DefaultNo): the cost of a wrong yes is data leaving the machine. An approval (interactiveState.claudeApproved) holds for the rest of the session on every route, so the question is not repeated; it is not persisted —escalation_agentis how to make Claude the configured agent. - One ask layer, every surface.
gateClaude(cmd/milk/fallback_core.go) races the TUI prompt, the ACP permission dialog and Telegram throughaskPermission(ADR-0052's foreground policy). Single-prompt mode asks on the terminal when stdin is one and otherwise refuses, namingescalation_agent. With no surface to ask, the answer is no. - Declining is not a failure. The turn is not run and not recorded (
errFallbackDeclined, shown as a notice; ACP ends the turn normally). A declinedescalate()is recorded as an assistant note so the request does not look unanswered. - An approved outage fallback does not make escalation sticky (
noteTurnSucceeded): the approval is for Claude, not for pinning the session there after the primary recovers. - One deterministic refusal. A turn headed to the implicit Claude default while Claude Code is not installed fails up front with a clear message (a local
claude --versioncheck). A failed health ping is never grounds to refuse a turn: it is a short GET that a proxy hiccup fails for every agent on the same host, so when both agents read unavailable the router's own choice still runs and the real request (with its retries and its real error) decides. - Startup/reload warnings and the empty-state banner say that Claude will be asked about, rather than that turns are being routed to escalation.
Consequences¶
- No session ends up quietly on a cloud model; the first Claude use of a session costs one keystroke.
- Users who want the old automatic behavior set
escalation_agent: "claude". - Unattended callers (single-prompt in a pipe, ACP clients that cannot answer permission requests) fail or decline rather than escalate.
- Not covered: workflow stages that name the
escalationrole explicitly in their YAML, and a primary that fails mid-turn (reported as before).