Skip to content

detach --purge then attach no longer downgrades claude to base URL in silence - #822

Open
philcunliffe wants to merge 6 commits into
masterfrom
fix/issue-819
Open

detach --purge then attach no longer downgrades claude to base URL in silence#822
philcunliffe wants to merge 6 commits into
masterfrom
fix/issue-819

Conversation

@philcunliffe

@philcunliffe philcunliffe commented Aug 17, 2026

Copy link
Copy Markdown
Contributor

The defect

On a real macOS install, hyp detach claude --purge followed by hyp attach claude reports success and writes a base-URL attach. No CA is minted, no keychain dialog appears, Remote Control inbound stays dead, and no surface says a thing: hyp status reports healthy.

Three settled behaviours compose into it, each right on its own:

  1. Attach reads the mode off the CA file, never off config (LLP 0232 #proxy-attach-preflight).
  2. --purge deletes the CA (LLP 0238 #ca-survives-detach) and never touches config, so proxy_mode: true survives with no CA behind it; the running daemon holds its loaded CA in memory and never re-mints.
  3. The LLP 0244 migration offer returns early on proxy_mode: true, because its own question is about writing that key.

Root cause

The attach-time gate at src/core/commands/clients.js:

if (effectiveGateway?.config?.proxy_mode === true) return

proxy_mode: true covers two states and only one is settled:

Config CA on disk Meaning
proxy_mode: true present genuinely in proxy mode, nothing to do
proxy_mode: true absent config and machine disagree, repair needed

The repair already exists and already works. maybeOfferProxyModeMigration runs before the endpoint is resolved precisely so an accepted switch can restart the daemon and let attach() find the fresh CA. It just declines to fire in the one state that needs it.

What changed

The gate reads config and the CA. With both, unchanged and silent. With the key and no CA, the attach reaches a new branch.

The downgrade is named on every attach shape (non-TTY, --json, hyp attach all included; --dry-run stays the one silent shape), on stderr, before any question, with the manual repair in the line. A user who reads only that line can fix the machine by hand.

The repair is a daemon restart, and it writes nothing. An interactive, single-client, wet-run attach offers it; accepting reuses enableGatewayProxyMode's restart/bind-wait/CA-wait steps with the config write skipped, and the attach below then takes the proxy branch it already has. Because nothing is written it has nothing for the LLP 0031 merge to drop, so it repairs a fleet-managed host too, where LLP 0244's migration has to decline. New outcome on that function: remint. already now means "config right and CA present". Where no daemon service is installed, the existing install-and-start ladder is named instead of a repair being claimed.

hyp status names the state. A proxy_mode_ca_missing warning diagnostic on both the text and --json surfaces, on every platform (the keychain/launchd trust block is darwin-only by design; a config asking for a transport the machine cannot serve is not). This is the missing half of a pair whose inverse (proxy mode off, CA present) the gateway already warns about as aigw.proxy_mode_stale_ca. Warning and non-degrading: a freshly-configured install whose daemon has not started yet is legitimately here for a few seconds.

--purge itself is unchanged. Re-minting on the way out contradicts what purge promises, and clearing proxy_mode cannot work on a fleet host, where that key belongs to the central layer.

The new LLP

LLP 0259 (Decision, Accepted) records this. It was needed for two reasons:

  • LLP 0244 #attach-offers settles that "the offer is keyed on the config, not the CA". This adds a CA-derived trigger to that gate, so it changes what an Accepted doc settled. 0244 gains an Extended-by: forward-ref; nothing in it was rewritten.
  • LLP 0232 #proxy-attach-preflight contradicts itself, saying both "a missing CA is a refusal (markActionRefused), not a warning, and nothing is written" and, a paragraph later, "attach uses proxy mode when a CA exists and base-URL mode otherwise". Only the second was ever built (the adapter's one CA_MISSING refusal sits behind proxy mode having already been selected, so it cannot fire here). 0259 settles it in favour of the fallback, and repairs what made the fallback unsafe: refusing outright would break the ordinary base-URL install, which is most installs. The fallback was never the bug. The silence was.

Verification

Regression tests fail on origin/master and pass here (11 failing before, 0 after):

  • test/core/attach-proxy-migration.test.js: the purge-then-attach sequence, with the fake adapter now recording the mode the way LLP 0232's preflight decides it, so the downgrade is visible to the test. Covers decline, accept, non-TTY, attach all, --dry-run, and the CA-present case that must stay silent.
  • test/core/gateway-proxy-enable.test.js: the remint outcome, the untouched config and absent backup, the fleet-managed host, the no-daemon case, and the CA timeout.
  • test/core/status-proxy-mode-ca-missing.test.js (new): the diagnostic on both surfaces, on darwin and linux, and its absence in the two states that are not faults.

npm test: 4267 pass, 4 fail. Those 4 are the pre-existing union-source absent-column failures on master (#820), identical to the pristine origin/master baseline captured in the same worktree. npm run typecheck clean. Smokes client_attach_idempotent, claude_attach_detach, status_diagnostics, walkthrough_picker_to_first_query all green.

The darwin half (keychain trust, the real dialog) cannot be proven on Linux; a release touching this path wants a docs/ACCEPTANCE.md pass on a real Mac.

Fixes #819

test and others added 3 commits August 17, 2026 22:49
… silence

`hyp detach claude --purge` deletes the local CA by design and leaves
`proxy_mode: true` in config, and the running daemon has no reason to
re-mint. The next `hyp attach claude` therefore read the CA-derived
preflight (LLP 0232), found nothing, and wrote a base-URL marker while
reporting success. `hyp status` said nothing either: its proxy-trust block
is built from the CA, so with no CA the whole block vanished and the
install reported healthy with Remote Control inbound dead.

Root cause is the attach-time gate: `proxy_mode: true` returned early as
"nothing to do", which is true only when a CA also exists.

- The gate now reads config AND the CA. With both, unchanged. With the key
  and no CA, the attach names the downgrade on every shape (non-TTY,
  --json and `attach all` included) and, interactively, offers the repair.
- The repair is `enableGatewayProxyMode` with the write skipped: a daemon
  restart makes the gateway re-mint, then the existing attach takes the
  proxy branch. Because it writes nothing it also repairs a fleet-managed
  host, where LLP 0244's migration must decline. New outcome, `remint`.
- `hyp status` gains a `proxy_mode_ca_missing` warning naming the state and
  the repair, on the text and --json surfaces, on every platform.

LLP 0259 records the decision: it extends LLP 0244 (whose gate is keyed on
config alone, correct for the migration question) and settles LLP 0232
#proxy-attach-preflight, which said both "a missing CA is a refusal" and
"base-URL mode otherwise". The fallback is what was built and stays; the
silence is what was wrong.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…e LLP 0259 paths

- maybeOfferProxyModeMigration's new remint branch claimed 'daemon restarted,
  local CA re-minted' without checking daemonInstalled, so a host with no
  service was told a repair ran that could not have.
- repairStrandedProxyMode had no branch for outcome 'enabled', reachable when
  enableGatewayProxyMode's layered read throws and it falls back to the local
  file, so a repair that succeeded was reported as a failure.
- The proxy_mode_ca_missing status diagnostic ignored 'enabled: false' on the
  gateway entry, unlike every other plugin lookup in status.js. A disabled
  gateway never mints, so the named repair could never clear the warning.

Each fix has a regression test that fails on 3b4776c.
@philcunliffe

Copy link
Copy Markdown
Contributor Author

Verdict

Approve with fixes applied. The diagnosis is right, the root-cause fix is the right shape, and LLP 0259 settles a real contradiction rather than inventing one. Three findings, all in the reporting half of the change (the half this PR exists to get right), all fixed on the branch with regression tests that fail on 3b4776c6. Nothing left actionable.

Reviewed at 3b4776c6640c341806a9dc1f4eb20be58ef2ffea in a clean worktree. npm test 4271/4271 green at the head SHA (the four #820 union-source failures the PR body mentions are gone: #821 merged into this branch fixed them), npm run typecheck clean, smokes client_attach_idempotent and status_diagnostics green.

Independently verified

The LLP 0232 contradiction is real. llp/0232-claude-attaches-by-proxy.decision.md:47-64, section ### Proxy-attach preflight, says in one paragraph:

A missing CA is a refusal (markActionRefused), not a warning, and nothing is written.

and in the next:

attach uses proxy mode when a CA exists and base-URL mode otherwise.

Those cannot both hold. And the code claim checks out too: the adapter's only CA_MISSING refusal is at hypaware-core/plugins-workspace/claude/src/settings.js:186, inside if (mode === MODE_PROXY) { at line 169. The mode is selected in hypaware-core/plugins-workspace/claude/src/index.js:200 from the CA's presence, so on the purge path mode is never MODE_PROXY and the refusal is unreachable. Only the fallback was ever built.

LLP 0259's call is the right one. The refusal sentence, read at the seam it governs, means the adapter refuses whenever no CA exists - and the adapter cannot see config, so that is every ordinary base-URL install, not just the stranded one. Settling in favour of the fallback and fixing the silence is correct. Two things I checked because they are what would make the call wrong:

  • Declining the repair is now an informed downgrade, not a silent one, and the exit code stays 0 so scripted attaches keep working (test/core/attach-proxy-migration.test.js non-TTY case).
  • A base-URL attach still captures; what it loses is Remote Control inbound, which the warning names explicitly. Degrading loudly beats refusing.

LLP immutability holds. llp/0232 gained four lines appended to its existing Extended-by: paragraph; llp/0244 gained a new Extended-by: line. Nothing either doc settled was rewritten (git diff origin/master...3b4776c6 -- llp/ shows 6 and 4 lines touched, header-only). All four LLP 0259# anchors used in code resolve to {#...} in the doc. 0259 collides with nothing on any branch.

The gate genuinely distinguishes the two states. src/core/commands/clients.js:856-860 splits proxy_mode === true into CA-present (returns, byte-identical to the old behavior) and CA-absent (the new branch). The three ordinary paths are provably untouched: parsed.dryRun returns at line 833 before the gate; a client whose descriptor lacks compose.gateway_proxy_mode returns at line 838; a config without proxy_mode falls through to the unchanged LLP 0244 migration.

The user-visible surface is right. repairStrandedProxyMode (src/core/commands/clients.js:1037) writes the warning before the shape check, so non-TTY, --json and attach all all get it; --dry-run never reaches the function. Warning goes to stderr, so --json stdout stays parseable (I added a test asserting exactly that - it was the one shape the PR claimed but did not cover).

remint writes no config. src/core/config/gateway_proxy_enable.js:151-170 sets remint and skips both the central/no-gateway decline at line 179 and the whole Step 1 write block at line 197. Verified by test/core/gateway-proxy-enable.test.js:178 ("a centrally-managed gateway is still re-minted: no write to collide") - the fleet-managed claim holds.

The status diagnostic is non-degrading. warning severity, and proxy_mode_ca_missing is not in degradingKinds (src/core/daemon/status.js:1265), so overall stays healthy. It does fire on a configured-but-never-started install, which LLP 0259 settles deliberately; I checked the named repair and it is safe there, because hyp daemon restart self-diagnoses the no-service case and prints the install ladder itself (src/core/commands/daemon.js:157-162). Not a finding.

Findings

1. remint in the migration path claims a restart that never ran - medium - FIXED

src/core/commands/clients.js:967 (pre-fix). The new remint branch printed

✓ proxy mode restored (daemon restarted, local CA re-minted)

with no result.daemonInstalled check, unlike the enabled branch six lines above it and unlike repairStrandedProxyMode's own handler. enableGatewayProxyMode returns ok: true, outcome: 'remint', daemonInstalled: false when no service is installed and nothing at all ran - this PR's own test/core/gateway-proxy-enable.test.js:204 asserts that contract. Reachable whenever the key lands on disk between ctx.config being read and the write (the function's gate reads ctx.config; enableGatewayProxyMode re-reads disk). The user is told the machine was repaired, the attach below still lands in base-URL mode, and no surface says otherwise - the precise failure this PR exists to remove.

Fixed by mirroring the sibling handler: daemonInstalled: false now names the install-and-start ladder on stderr. Test: an accepted migration that turns out to be a re-mint never claims a restart that could not run.

2. repairStrandedProxyMode reports a successful repair as a failure - low-medium - FIXED

src/core/commands/clients.js:1105-1128 (pre-fix) handled already and remint but not outcome === 'enabled', so a successful write fell through to warning: could not restore proxy mode ...; attaching by base URL instead. Reachable when resolveLayeredConfigFromDisk throws inside enableGatewayProxyMode (caught and continued by design, src/core/config/gateway_proxy_enable.js:138-141): effectiveGateway is then undefined, remint is false, the write path runs and reports enabled, the daemon restarts, the CA is minted - and the user is told it failed while the attach actually lands in proxy mode. The exact inverse of finding 1, and equally a lie about the machine.

Fixed with an enabled branch that reports success and honours daemonInstalled. Test: a repair that comes back as a config write reports success, not a failure.

3. proxy_mode_ca_missing fires on a disabled gateway - low - FIXED

src/core/daemon/status.js:1229 (pre-fix) found the gateway with .find(entry => entry.name === '@hypaware/ai-gateway') and never read entry.enabled === false. Every other plugin lookup in the same file does (lines 709, 1430, 1459). A user who switches capture off with enabled: false keeps proxy_mode: true inside the disabled block - nothing rewrites it - and hyp status then warns permanently with a repair that can never clear it, because a disabled source never launches prepareInterception and never mints. This is the "fires spuriously" case, and unlike the fresh-install case it never resolves.

Fixed with gatewayEntry?.enabled !== false, plus one sentence in LLP 0259 #status-names-it recording the carve-out (the doc lands in this PR, so this is review iteration, not an edit to a settled record; it extends the doc's own "asking for a transport" rationale rather than reversing anything). Test: a disabled gateway carrying a stale proxy_mode is not a fault.

Nits (not fixed, no action needed)

  • hasLocalCa's JSDoc (src/core/commands/clients.js:988) says an unreadable CA "counts as absent, the same reading readLocalCaInfo hands the adapters". Half true: readLocalCaInfo returns undefined for ENOENT and for a malformed PEM, but throws on other read errors (EACCES), which the adapter does not catch. The added try/catch is right and errs safe; only the sentence overstates the symmetry.
  • attach all would print the warning once per proxy-attach client. Today only claude declares compose.gateway_proxy_mode, so it is once; worth remembering when a second one lands.

Needs a real-Mac acceptance run

Everything below the CA-file abstraction is Linux-provable; everything above it is not. Before a release that ships this, run on a real macOS host:

  1. The #819 repro end to end: hyp attach claude in proxy mode, hyp detach claude --purge, hyp attach claude. Confirm the downgrade warning appears, accept the repair, and confirm exactly one keychain trust dialog follows (the two-dialog report in #793 is the neighbouring bug; this path must not add a third).
  2. hyp status on the stranded machine before the repair: proxy_mode_ca_missing present and overall: healthy, and the proxy trust block absent (there is genuinely no certificate to describe).
  3. After the repair: launchd NODE_USE_SYSTEM_CA set, NODE_EXTRA_CA_CERTS/HTTPS_PROXY written into Claude's settings, and Remote Control inbound actually alive - the failure the user reported, which no unit test on Linux can observe.
  4. Decline the repair once and confirm the base-URL attach still works and hyp attach claude a second time still warns (the warning must not be a once-per-machine notice).

Worth a docs/ACCEPTANCE.md entry for the sequence, since it is now a documented lifecycle state rather than an accident.

Pushed

6294f0f4 on fix/issue-819. Post-fix: npm test 4275/4275, npm run typecheck clean, both smokes green. Each of the three fixes was verified by stashing it and watching only its own test go red.

…top at the machine

On a daemon-managed install (no configured `listen`) attach short-circuits at
"already attached" whenever the client's marker names the live port. A daemon
restart keeps that port, so after an accepted re-mint the marker still matched
and the adapter never ran: the CLI printed the repair's success line while the
client stayed in the mode the repair was meant to leave behind, and the darwin
trust grant (which only runs inside attach()) never re-ran either. That is the
silent wrong outcome LLP 0259 exists to remove, reached by declining the offer
once and accepting it on the next run.

maybeOfferProxyModeMigration / repairStrandedProxyMode now report whether the
proxy state actually moved, and a move disqualifies the no-op when there is a
live endpoint to re-attach at. An offer that changed nothing (declined, or
reported with no daemon to restart) leaves the LLP 0086 no-op untouched.

Also corrects two JSDoc sentences that overstated readLocalCaInfo's contract:
it returns undefined for a missing or malformed cert but throws on any other
read failure, which the adapters do not catch.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@philcunliffe

Copy link
Copy Markdown
Contributor Author

Verdict

Approve with one fix applied. Round 1's three fixes hold under scrutiny and introduced no misreporting of their own. The round-2 sweep ("can this line be printed when the thing it describes did not happen?") turned up one more instance of the PR's own bug shape, in the half nobody had looked at: the repair reaches the machine and can stop short of the client. Fixed on the branch with a regression test that fails at 6294f0f4. Two nits remain unactionable, and one cross-PR conflict with #817 needs a decision by whoever merges second.

Reviewed at 6294f0f4bb470bbf93745a83e3b9d71ff37caa90 in a clean worktree off origin/fix/issue-819. Post-fix: npm test 4277 pass / 0 fail / 1 skipped, npm run typecheck clean, smokes client_attach_idempotent, status_diagnostics, claude_attach_detach, walkthrough_picker_to_first_query all green.

Round 1's three fixes: verified, and complete

Each re-derived from the code rather than from the round-1 record.

  1. remint in the migration path. src/core/commands/clients.js:984-1000 now splits on result.daemonInstalled. Checked the claim that backs the success line: on the remint path enableGatewayProxyMode reaches ok: true only after the bind wait and the CA wait both succeed (src/core/config/gateway_proxy_enable.js:274-305), so "daemon restarted, local CA re-minted" is a verified fact at that point, not an inference. The daemonInstalled: false early return at line 244 is the only other ok: true exit and it is now handled. No third path.
  2. enabled reported as a failure. src/core/commands/clients.js:1151-1166 handles it and honours daemonInstalled. Reachable exactly as round 1 described (the catch at gateway_proxy_enable.js:138-141 leaves effectiveGateway undefined, so remint is false and the write path runs).
  3. proxy_mode_ca_missing on a disabled gateway. src/core/daemon/status.js:1234 reads gatewayEntry?.enabled !== false, matching the other lookups in the file. Correct.

None of the three introduced a new claim that outruns what was checked.

The "can this message lie?" sweep

I walked every string these paths can print. Results:

Honest, verified: all four lines in repairStrandedProxyMode and both in the migration path (each is guarded by result.ok plus daemonInstalled, and ok on the restart paths implies the CA wait passed); the two no daemon service is installed warnings; the could not restore/switch to proxy mode fallbacks (they are the !ok catch-all and name result.message); the non-TTY / --json / attach all pointer note; the decline line.

Not honest in one shape - finding 1 below. Every one of those lines describes the machine, and on the install shape they most often run on the run could still end without the client being touched.

I also checked enableGatewayProxyMode's span/log attributes track result.outcome rather than a hardcoded 'enabled' (they do, lines 249 and 305), so the telemetry does not contradict the text.

Findings

1. An accepted repair can print its success line over a client it never reached - medium - FIXED

src/core/commands/clients.js:359-363 (pre-fix). On a daemon-managed install (no listen configured, which is the default and, per that branch's own comment at line 391, "the one an operator on a daemon-managed install actually reaches"), gateway.localEndpoint() throws, configuredGatewayEndpoint returns undefined, and attach falls into the LLP 0086 live-port branch. There, alreadyCurrent short-circuits with continue whenever the client's marker names the live port - and a daemon restart keeps the port. So:

  • hyp attach claude on a stranded install: warning printed, repair declined, base-URL marker written at the live port.
  • hyp attach claude again: warning printed, repair accepted, daemon restarts, CA is re-minted, CLI prints ✓ proxy mode restored (daemon restarted, local CA re-minted) - then the marker still matches the live port, alreadyCurrent fires, and the adapter's attach() never runs.

The client stays on ANTHROPIC_BASE_URL, and ensureDarwinProxyTrust (hypaware-core/plugins-workspace/claude/src/index.js:228, reachable only from inside attach()) never re-runs, so the keychain trust --purge removed is never re-granted. Exit 0, success line printed, machine still downgraded. That is #819's own failure mode one layer up, and LLP 0259 #repair-is-a-restart's "it fixes the state however it was reached" does not hold for it.

Same short-circuit also swallows the LLP 0244 migration's ✓ proxy mode enabled on that install shape, and reaches a second scenario that needs no decline at all: purgeProxyTrustResidue (src/core/commands/clients.js:1793) is per-invocation, not per-client, so hyp detach codex --purge deletes the machine-wide CA while claude is still attached.

Fix. maybeOfferProxyModeMigration and repairStrandedProxyMode now return whether the proxy state actually moved, and a move disqualifies the no-op - but only when a live endpoint was discovered, so with no daemon to re-attach at a present marker still beats the give-up error. An offer that changed nothing (declined, or ok with daemonInstalled: false, where nothing was restarted and no CA exists) leaves the LLP 0086 no-op exactly as it was.

Tests in test/core/attach-proxy-migration.test.js: a repair that lands re-attaches the client instead of no-opping on its pre-repair marker (new unbound ctx shape + seeded status.json + marker at the live port; the CA is staged synchronously as the downgrade warning is written, which is the already race the code already documents) and its control a declined repair leaves the already-attached no-op exactly as it was. Verified by removing only the new condition and watching only the first go red.

One sentence added to LLP 0259 #repair-is-a-restart recording it. The doc is minted by this PR, so this is review iteration, not an edit to a settled record.

2. readLocalCaInfo symmetry overstated in two JSDoc blocks - low - FIXED

Round 1 left this as a nit; it is a two-line doc-only fix with no risk, and this is the last round, so I took it. readLocalCaInfo (src/core/tls/ca.js:355-376) returns undefined for ENOENT and for a malformed PEM but throws on any other read error, and the adapter at claude/src/index.js:200 does not catch. hasLocalCa (clients.js) and hasLocalCaOnDisk (status.js) both claimed their catch was "the same reading readLocalCaInfo hands the adapters". Both now say what is actually true and why catching here errs the safe way (warning plus repair offer, not silence).

Left, deliberately

  • The status diagnostic clears while a base-URL attach persists. proxy_mode_ca_missing is keyed purely on CA absence, so decline the repair, reboot, let the daemon re-mint, and hyp status is healthy again with claude still on ANTHROPIC_BASE_URL. Real, but it is a different diagnostic (attached-mode vs proxy_mode), it needs data probeClientAttachFromDescriptor does not currently return (src/core/daemon/status.js:1676-1681 carries version and port, not the managed env key), and LLP 0259 #status-names-it deliberately settles this one as the config-versus-disk pair. Worth a follow-up issue, not a change here. Note that finding 1's fix narrows the window: the second attach now completes the repair.
  • Platform wording. The repair prompt says "macOS will then ask to trust the HypAware Local CA" and the warning says a base-URL attach "breaks Remote Control inbound", both unconditionally, while LLP 0237 makes Remote Control inbound darwin-only (the adapter itself says so on other platforms, claude/src/index.js:241). Both phrasings are copied verbatim from the pre-existing LLP 0244 migration prompt at clients.js:939-942; diverging one of the pair here would be worse than the inconsistency. Fix both together or neither.
  • attach all repeating the warning. Still once: compose.gateway_proxy_mode: true is declared by exactly one manifest (hypaware-core/plugins-workspace/claude/hypaware.plugin.json:45). Not actionable until a second proxy-attach client lands.
  • maybeOfferProxyModeMigration's gateway lookup does not read enabled: false, unlike the status one round 1 fixed. It does not mislead: a disabled gateway never binds, so the repair fails at the bind wait and reports could not restore proxy mode (...did not publish a bound port in time). Costs a wasted restart in a state attach can barely reach anyway (it needs the live gateway capability to resolve the client at all).

LLP hygiene

  • llp/0232 and llp/0244 carry only Extended-by: forward-refs (0232:9, 0244:10); no Superseded-by, and nothing either doc settled was rewritten.
  • All five LLP 0259# anchors used in code resolve to {#...} headings in the doc (#gate-reads-both, #never-silent, #repair-is-a-restart, #status-names-it).
  • #status-names-it is accurate after round 1: its enabled: false paragraph matches status.js:1234 exactly, and its "warning, not degrading" claim matches proxy_mode_ca_missing being absent from degradingKinds.

Interaction with #817 - needs a decision, plainly

#817's LLP 0251 #migration will be false the moment this PR lands. Four sentences, stated flatly:

  • "It offers only when ... the effective gateway block lacks proxy_mode: true" - no longer true; the gate now also fires when the key is present and the CA is not.
  • "The offer is keyed on the config, not the CA" - this PR amends that exact JSDoc sentence in clients.js to scope it to the migration question. LLP 0251 quotes code text that will not exist.
  • "Idempotence. Once the key is set the offer never appears again" - directly contradicted; the whole point of detach --purge then attach no longer downgrades claude to base URL in silence #822 is that the key being set is not sufficient.
  • "Fleet-managed gateway: no local write ever; report-only" - the write half survives, the report-only half does not: a re-mint restarts the daemon on a fleet host by design (LLP 0259 #repair-is-a-restart).

§4's failure-mode list and §3's "The write." paragraph also predate the no-write remint outcome. I have not touched #817. Whoever merges second should reconcile: the cheapest form is an Extended-by: LLP 0259 line on 0251 plus a scoping word in those four sentences, since 0251 is Status: Active and a design of record.

Also expect a textual conflict. Both PRs edit test/core/attach-proxy-migration.test.js, and both change the same makeCtx helper (#817 changes its signature and the fake adapter's attach; #822 changes the fake adapter's attach to record the CA-derived mode, and now adds an unbound/onStderr shape). Both also touch llp/0244's header block, though on different lines.

Needs a real-Mac acceptance run

Unchanged from round 1, plus one item finding 1 adds. Everything below the CA-file abstraction is Linux-provable; nothing above it is.

  1. The detach --purge then attach silently downgrades claude to base-URL mode, with no warning on any surface #819 repro end to end: attach in proxy mode, hyp detach claude --purge, hyp attach claude. Downgrade warning appears; accept; exactly one keychain dialog follows (Follow-up: F1 and F2 from #790, plus proxy-trust status findings deferred from PR #792 #793's two-dialog report is the neighbouring bug and this path must not add a third).
  2. New, and the one finding 1 is about: decline the repair, then run hyp attach claude a second time and accept. Confirm the client's settings.json actually flips from ANTHROPIC_BASE_URL back to HTTPS_PROXY + NODE_EXTRA_CA_CERTS, and that the keychain trust is re-granted. On a Linux CI box this is only provable through the fake adapter.
  3. hyp status on the stranded machine before the repair: proxy_mode_ca_missing present, overall: healthy, proxy trust block absent.
  4. After the repair: launchd NODE_USE_SYSTEM_CA set, and Remote Control inbound actually alive.

Worth a docs/ACCEPTANCE.md entry, since this is now a documented lifecycle state rather than an accident.

Pushed

7bd43f633c15e3ced5f1abda56bc9d85dfd921cd on fix/issue-819.

@philcunliffe

Copy link
Copy Markdown
Contributor Author

Triage at head 7bd43f633c15e3ced5f1abda56bc9d85dfd921cd: no blocking residue. Both review rounds' fixes verified present in the tree; npm test 4277/0, typecheck clean, smokes client_attach_idempotent and status_diagnostics green.

Every deferred finding is tracked in #832, including:

  • the status-diagnostic gap (decline the repair, let a reboot re-mint the CA, and every surface goes quiet over a client still on ANTHROPIC_BASE_URL) - judged a follow-up, not a blocker: the remaining message is incomplete rather than false, the state is only reachable through an informed decline, master today reaches the same terminal state with no warning at all, and the fix is a different diagnostic needing probe data this PR does not touch;
  • the Proxy mode by default: design of record for LLP 0242, plus the missing --json non-interactive pin #817 merge-time action (llp/0244 metadata conflict plus four LLP 0251 #migration sentences going stale), cross-referenced with Follow-up: deferred review findings from PR #817 #830;
  • the platform-wording and enabled: false symmetry items;
  • the real-Mac acceptance sequence both rounds listed.

@philcunliffe philcunliffe added the neutral:approved neutral reviewed this and holds it for a maintainer merge (own or adopted PR; LLP 0025/0030) label Aug 18, 2026
@philcunliffe philcunliffe removed the neutral:approved neutral reviewed this and holds it for a maintainer merge (own or adopted PR; LLP 0025/0030) label Aug 18, 2026
Three conflicts, all additive: LLP 0232 keeps 0259's Extended-by clause
beside master's Superseded-by (in part) for 0262/0258; LLP 0244 carries
both Extended-by clauses (0259's CA-reading gate, 0262's OTEL retarget);
StatusDiagnosticKind takes both new kinds, proxy_mode_ca_missing and
capture_gap.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@philcunliffe philcunliffe added the neutral:stuck neutral attempted this but cannot complete it autonomously — needs a human label Aug 19, 2026
@philcunliffe

Copy link
Copy Markdown
Contributor Author

Neutral triage: cannot ship at this head, needs a human decision

What neutral was doing. Triage after 2 review rounds on PR #822. Both rounds' findings were verified fixed and the residue was deferred to #832 at head 7bd43f63. The head has since moved to 5255cf0f, a merge of origin/master, so that triage no longer covers the PR.

Why it cannot proceed. The merge brought in master's 8f740a3f (OTEL telemetry attach replaces proxy capture for Claude Code, LLP 0262/0258), and the merge commit bf43f829 resolved only three additive header conflicts. Nobody has reconciled this PR's centerpiece with the new attach model, and at the merged head the two contradict each other. This is a true blocker: wrong user-facing behaviour on exactly the machines this PR targets.

The blocker, concretely

At head, the claude adapter attaches only via OTEL and never by proxy:

  • hypaware-core/plugins-workspace/claude/src/index.js:184-189: "otel is the claude client's only attach mode: a machine still carrying a proxy attach is migrated by this same write ... never re-attached by proxy" (@ref LLP 0258#version-floor: one attach mode per client, with no proxy fallback). ensureDarwinProxyTrust is gone; nothing in the OTEL attach path touches the keychain.
  • llp/0232-claude-attaches-by-proxy.decision.md:20-23: now "Superseded-by (in part): LLP 0262, LLP 0258".

Yet this PR's repairStrandedProxyMode (src/core/commands/clients.js:1075) still fires on every hyp attach claude where config says proxy_mode: true and no CA is on disk (the gate at clients.js:874-877), which is every machine that attached under the proxy-default era and later ran detach --purge, i.e. the #819 population itself. On that machine it now:

  1. Prints a false warning on every attach shape (clients.js:1076-1080): "attaching claude now writes a base-URL attach instead (which breaks Remote Control inbound)". At head the attach writes an OTEL env block, never a base URL, and Remote Control keeps working. The state the warning describes as a downgrade is harmless for claude now.
  2. Prompts with promises attach cannot keep (clients.js:1097-1101): "Claude can be put back on HypAware's local HTTPS proxy by restarting the daemon ... macOS will then ask to trust the HypAware Local CA". No trust dialog will appear and claude will not be put on the proxy.
  3. On accept, restarts the daemon (briefly interrupting the gateway for every other client), re-mints a CA that claude's attach will never use, and prints "proxy mode restored (daemon restarted, local CA re-minted)" (clients.js:1140), after which the attach below writes MODE_OTEL. On a machine still carrying a proxy marker, the adapter then prints migration notes that directly contradict the line above ("the proxy env keys are released and Claude Code talks to Anthropic directly again", and "run 'hyp detach claude --purge'" for the CA that was just deliberately re-minted). A success line that lies about the machine is the precise defect class this PR exists to remove.
  4. The non-interactive note (clients.js:1089-1091) tells scripted callers to run a daemon restart "to restore the proxy attach", advice that cannot work.

The LLP corpus also lands in contradiction: LLP 0259 (Accepted, minted by this PR) settles that attach repairs a stranded install back into a proxy attach for claude, while LLP 0258 (Accepted, on master) settles one attach mode per client with no proxy fallback. Per repo rules two Accepted docs must not contradict; this PR would create that state.

Why no gate caught it. Both review rounds (at 3b4776c6 and 6294f0f4) predate the OTEL change entering this branch. The full suite at head is green (4503 pass, 0 fail) because the PR's tests drive a fake adapter that still models the CA-derived proxy/base-URL choice the real adapter no longer has.

Non-blocking residue (unchanged, for the whole picture)

All already tracked in #832: the status-diagnostic gap after an informed decline, platform wording shared with the LLP 0244 prompt, the enabled: false symmetry nit in the migration lookup, and the real-Mac acceptance sequence. The #817 LLP 0251 interaction is cross-referenced in #830. The proxy_mode_ca_missing status diagnostic itself (config asks for a transport the machine cannot serve) remains coherent and non-degrading at head.

Decision needed

How to reconcile this PR with LLP 0262/0258. The plausible shapes:

  1. Retarget: keep the status diagnostic and the config-versus-disk honesty, but rewrite or drop the attach-time warning and repair for claude, since OTEL attach makes the stranded state harmless to this client. LLP 0259 lands in this PR, so rewriting it is still review iteration, not an edit to a settled record.
  2. Scope the repair to proxy-attach clients only, which today is an empty set (only claude declares compose.gateway_proxy_mode), effectively deferring it until a second proxy client exists.
  3. Close the PR as overtaken: detach --purge then attach silently downgrades claude to base-URL mode, with no warning on any surface #819's silent downgrade no longer exists at head because there is no base-URL downgrade to fall into.

Neutral will not choose among these: each discards reviewed, working code or re-scopes an Accepted doc, and the sequencing question (this PR versus the LLP 0262 rollout) is a maintainer call.

To unstick: reply with a comment on this PR (or push to the branch). Neutral monitors this thread and will re-engage with your guidance on its next tick.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

neutral:stuck neutral attempted this but cannot complete it autonomously — needs a human

Projects

None yet

Development

Successfully merging this pull request may close these issues.

detach --purge then attach silently downgrades claude to base-URL mode, with no warning on any surface

1 participant