Type: Decision Status: Accepted Systems: Plugins, Onboarding, Config Author: Brendan / Claude Date: 2026-07-27 Related: LLP 0016, LLP 0024, LLP 0041, LLP 0153, LLP 0116, LLP 0117, LLP 0130, LLP 0131, LLP 0133
LLP 0133 shipped
hyp claude-desktop installand gave the plugin aneeds_setuppicker row. The row was never reachable: it composed nothing, so ticking it wrote a config the row's ownconfigure_commandcould not run in. This decision makes the row compose its full dependency set and replaces the config-file friction that had been standing in for a credential opt-in with an explicit consent gate.
A picker row's composition is a fold over each picked descriptor's
compose contribution (LLP 0130).
The Claude Desktop row shipped needs_setup: true and
configure_command: "claude-desktop install" but no compose block, the
only one of the eight bundled rows without one. The fold skips a
descriptor with no compose, so ticking Claude Desktop wrote a config
containing neither @hypaware/claude-desktop nor the
@hypaware/claude-account its manifest requires.
The failure was a dead end rather than an error. The configure phase ran
claude-desktop install, which exited nonzero because the command's
plugin was absent from the config just written; drop-on-failure
(LLP 0131) printed
the catch-up hint hyp claude-desktop install, which failed identically
on every subsequent run. Because /Applications/Claude.app satisfies the
row's detect probe, the row arrived pre-checked for every user who had
Desktop installed.
Two adjacent surfaces made the state harder to diagnose than it should have been:
- Following the dispatch repair hint literally (adding only
@hypaware/claude-desktop, LLP 0153) produced a worse error, not a better one. The plugin fails itsrequireCapability('hypaware.anthropic-credential')call, so its commands never register and the dispatcher reportsunknown command 'claude-desktop status'. hyp statusofferedhyp attach --client claude-desktopas the repair for the resulting un-attached client. The plugin registers noctx.clientsadapter by design (LLP 0133), so that command answersunknown client 'claude-desktop'. (overall: healthyalongside it is correct and deliberate:client_attach_missingis non-degrading per LLP 0041 §failure-is-surfaced-not-fatal.)
The reason the row had been left inert was real. @hypaware/claude-account
is excluded from default activation because it holds an Anthropic
credential, and V1_EXCLUDED_FROM_DEFAULT treats holding a credential as
a deliberate plugins[] decision. Composing it from a checkbox appears to
route around that.
A needs_setup row
composes every plugin its configure_command needs, not just its own
adapter. The Desktop row composes the gateway, the anthropic upstream,
@hypaware/claude-account (mode: "subscription"), and
@hypaware/claude-desktop. PluginPickerCompose gains plugins[] beside
the singular plugin for rows that contribute more than one, under the
same gateway-relative placement rule.
Composing half a dependency set is strictly worse than composing none: the
adapter fails its capability requirement, so its commands never register
and the failure surfaces as unknown command rather than as the missing
capability. A test asserts the general rule across every bundled row, so a
future needs_setup row cannot ship inert the same way.
The credential decision is gated by an
explicit consent prompt at the point of action, not by config-file
friction. hyp claude-desktop install explains what it will change
before it changes anything: the gateway endpoint it points Desktop at, why
this client is different, the sign-in, the helper path, the residue clear
when residue is present, the root-owned plist and its sudo prompt, the
restart, and how to undo it. A user who reads only that block can predict
every file that changes.
Scope correction (LLP 0140). This gate covers the live-capture path only: the credential, the helper, and the managed plist. It is not the sole door Desktop content enters by. Desktop writes its sessions into
~/.claude/projects, so the@hypaware/claudebackfill imported them with no Desktop opt-in at all. Read "the picker tick is the consent point" below as scoped to live capture; history is gated separately, by transcript-entrypoint ownership.
Amended by LLP 0224: the explanation and every disclosed fact stand, but the screen now leads with the decision rather than the mechanism, the question after it defaults to yes and names the browser sign-in a yes may launch, and it is asked for a new pick only - a reconfigure's carried row is skipped, with the incomplete-setup state surfaced by
hyp status'sclient_attach_missingdiagnostic (§repair-must-be-runnable) instead of by re-asking. The row itself is never pre-checked by detection.
This is a better gate than the exclusion list was, for three reasons.
First, the acquisition of a credential was already attended: step 1 of
install runs claude-account login interactively and refuses outright
without a TTY, so composing the plugin only enables something that can
hold a credential. Second, a plugins[] edit is silent about what it
implies, whereas the prompt states the posture in the words that matter:
unlike Claude Code and Codex, which keep their own sign-in while HypAware
rewrites only a base URL, Desktop's third-party profile has no
credential-passthrough mode (LLP 0116),
so attaching it necessarily makes this machine hold an Anthropic
credential. Third, the picker tick is attended and specific, so it is a
real decision rather than a default.
The mode question resolves along the same seam: the picker is the solo
path, so it composes subscription. org_key is fleet policy and arrives
through the central layer on hyp join, never through the picker
(LLP 0117).
Superseded by LLP 0224 §one-question-default-yes: the prompt now defaults to yes. The non-answer rule below survives unchanged - every non-answer declines, with the hint, without hanging; only an explicit no declines an answered prompt. The original text is kept below as the record of the earlier reasoning.
The prompt defaults to no, unlike the backfill consent prompt, which defaults to yes. Backfill reads local files this machine already has; this acquires a credential, escalates to root, and writes a file outside the user's home. A bare enter must not do any of that. A decline is a no-op that exits nonzero, which is what routes the wizard onto its existing drop-on-failure path so the catch-up command is printed instead of Desktop being reported as attached.
Every non-answer is a no, and says so. A cancel (esc / ctrl-c), an absent
stdin, and a stdin that ends without a line all decline with the same hint
naming --yes and --print-commands. The last case is the one that
matters in practice: the dispatcher defaults ctx.stdin to
process.stdin, so an absent stream never occurs, while a redirected one
(hyp claude-desktop install < /dev/null) does. Waiting on a readline
answer there hung forever with the hint unprinted, which is the same
unattended-hang class as the --print-commands bug below.
Consent is asked once, not on every re-run: a machine whose plist already matches and whose helper is already written has been through the prompt, and LLP 0131's idempotent re-run has to stay cheap enough to use as the documented repair step.
Before any gate runs, the command refuses on a
non-macOS platform. Every surface it touches is macOS-specific: the
managed plist under /Library/Managed Preferences, the Claude-3p
residue under ~/Library/Application Support, the cfprefsd flush. The
wizard already cannot reach the flow elsewhere (the row's detect probe
stats /Applications/Claude.app), but a direct invocation had no gate,
and on Linux it would half-succeed: consent, login, and helper write all
run, then sudo mkdir -p '/Library/Managed Preferences' creates
root-owned junk at the filesystem root while configuring nothing.
install and verify now refuse up front, mutating nothing and naming
the platform: the same loud contract hyp daemon install already has for
an unsupported platform. The refusal gates the applying path only:
install --print-commands passes everywhere, by the same rule that lets
it skip consent (it applies nothing, #print-commands-applies-nothing).
(profile, status, and install-helper also stay ungated: rendering
the MDM payload or staging the helper touches no macOS surface and is
legitimately useful on a non-Mac admin box preparing a fleet push,
LLP 0133#one-surface.) Platform reach beyond this is a
repo-level scope fact, not this adapter's gap: core supports darwin and
linux only, Claude Desktop has no Linux build, and a Windows port is
gated on a core service backend plus live-test discovery of the Windows
policy surface (the method LLP 0133 records for macOS).
The gate lives in the command, so the
wizard inherits it. The configure phase already invokes
claude-desktop install in-process through ctx.commands.run
(LLP 0130),
so putting the prompt in the command covers the standalone and wizard
surfaces with one implementation. This follows LLP 0131's existing rule
that the wizard adds no second implementation of a configure command.
The seam activates a freshly
config-enabled command plugin, so the wizard can reach the gate at all.
The gate living in the command only covers the wizard if the wizard can
dispatch the command. It could not: hyp init boots the all-available
profile, which by construction never activates a V1_EXCLUDED_FROM_DEFAULT
plugin, and the activation set (with the command registry it populated) is
fixed at process start, while the picker writes its composed config later
in that same process. So on a first-run init the configure phase's
in-process claude-desktop install missed dispatch, exited 2, and
drop-on-failure printed the catch-up hint - the consent prompt was
unreachable from the surface it was built for, and only the standalone
re-run ever showed it.
The fix follows the entrypoint gate's rule (LLP 0140):
only a fresh read of the config reflects a write that happened after boot.
On a registry miss, ctx.commands.run re-reads the effective config from
disk and, when a config-profile boot of that fresh read would select the
plugin declaring the missed command's head token, activates it into the
running kernel together with its config-selected dependency closure, in
dependency order (for the Desktop row: @hypaware/claude-account first,
which provides the credential capability, then @hypaware/claude-desktop;
the gateway is already active under all-available). This is scoped to the
in-process seam: a shell-invoked hyp <cmd> boots with the config
profile, which already activates config-listed plugins before dispatch.
The exclusion list still governs defaults - nothing activates that the
effective config does not name, and the config names Desktop's plugins only
because the row was ticked. Activation is not the gate; the prompt is, and
it still defaults to no. On any failure the seam stays silent and the
dispatch miss path reports unavailable-plus-repair exactly as before
(LLP 0153).
--print-commands applies
nothing, including the non-privileged steps. It previously honored the
flag only for the plist write and the restart, still running the credential
login and helper write for real. That made the one flag whose purpose is
avoiding unattended side effects the flag most likely to hang: on a machine
that was not signed in it dropped into an interactive OAuth flow. All five
steps now print under the flag, which is also what makes it correct for the
consent gate to skip it.
A diagnostic's repair must be a
command that runs. client_attach_missing takes its repair from the
client plugin's own picker row configure_command when it has one, falling
back to hyp attach --client <name> otherwise. A client that declares
contributes.client for probe and status plumbing but registers no runtime
adapter has to name its own setup command, or the repair we print answers
unknown client.
- Ticking Claude Desktop in
hyp initnow works end to end: compose, explain, confirm, login, helper, residue, plist, restart. (Amended by LLP 0224: the confirm defaults to yes, names the sign-in launch, and is asked per pick, not per run.) - Declining leaves
@hypaware/claude-accountand@hypaware/claude-desktopin the written config with no credential and no plist. That is the converging state, not a broken one: the re-run repair works precisely because the plugins are present, andhyp statusnow names that re-run as the repair. V1_EXCLUDED_FROM_DEFAULTstill excludes both plugins from default activation. Nothing enables them without either a picker tick or an explicitplugins[]edit; the exclusion governs defaults, and the picker is not a default.--yesaccepts the changes in advance for scripted use. An unattended fleet is unaffected: MDM places the same plist and never reaches this command (LLP 0133#one-surface).