Skip to content

claude-desktop: drop the attach_probe core cannot read or reverse - #445

Merged
philcunliffe merged 4 commits into
masterfrom
fix/issue-444
Jul 29, 2026
Merged

claude-desktop: drop the attach_probe core cannot read or reverse#445
philcunliffe merged 4 commits into
masterfrom
fix/issue-444

Conversation

@philcunliffe

@philcunliffephilcunliffe commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

What was actually wrong

The issue's headline (always fails with MALFORMED_MARKER) turns out to be the
wrong symptom, but it is pointing at a real and worse bug underneath.

hypaware-core/plugins-workspace/claude-desktop/hypaware.plugin.json declared:

"attach_probe": {
"format": "json_path",
"settings_file": "/Library/Managed Preferences/com.anthropic.claudefordesktop.plist",
"marker_path": "inferenceCredentialHelper"
}

Reproduced against origin/master by driving detachClientFromDisk and
probeClientAttachFromDescriptor with the descriptor buildPluginCatalog
derives from that manifest:

on-disk state at the probed pathdetachstatus probe
absentchanged:falsenot attached
the plist claude-desktop install actually rendersthrows MALFORMED_JSONerror: Unexpected token '<'
a hypothetical JSON profilechanged:falsenot attached
inferenceCredentialHelper as an objectthrows MALFORMED_MARKERattached

MALFORMED_MARKER needs the last row, which cannot occur:
inferenceCredentialHelper is a plain string in the app's own schema
(LLP 0115#real-profile-keys). The failure a user with Desktop configured
actually hits is a silent, wrong "not attached": resolveClientSettingsPath
re-anchors the absolute settings_file under $HOME, so the probe never
reads the real, root-owned plist at /Library/Managed Preferences/... at
all - it reads a path that never exists - and probeClientAttachFromDescriptor
treats that ENOENT as attached:false with no error
(status.js:1012-1013). The table's MALFORMED_JSON row is real, but it is
what the probe throws only once that path is corrected and it actually
reaches the XML plist; the new test demonstrates this by writing that plist
directly at the re-anchored path, which is not where hyp claude-desktop install places it today.

(Corrected post-review: an earlier version of this line claimed the field
failure users actually hit is MALFORMED_JSON, firing "on exactly the
machines where install had succeeded". Round 2 found that backwards -
defect 2 masks defect 1 in the field, so the quiet wrong answer is what ships
today, not the loud one. llp/0135#no-probe records the corrected account;
this body line is fixed here as part of triage.)

The probe was wrong three independent ways, any one fatal:

  1. Wrong file type. The plist is XML. Every core json/json_path path
    JSON.parses the settings file.
  2. Wrong path.settings_file is $HOME-relative by contract
    (resolveClientSettingsPath), so the absolute /Library/... re-anchored to
    ~/Library/Managed Preferences/... and never named the real file.
  3. No undo record. No marker_record, and per LLP 0045 Part 3 a
    record-less marker is refused rather than half-reversed. Adding one would
    not help: the plist is root-owned, so an unprivileged detach cannot write it
    back at all.

The fix

Manifest-only (plus the doc and comment it made dishonest): remove the
attach_probe
.

This is not a new decision, it is restoring one.
LLP 0115#no-attach-on-join
is Accepted and says Desktop registers no contributes.client with an
attach_probe, because the LLP 0044 loop requires a reversible settings-file
write that Desktop cannot offer. LLP 0135's design bullet asked for one anyway,
"reflecting the entrypoint: claude-desktop-3p attribution finding" - a
category error, since an attach_probe is not a label but the input to
probeClientAttachFromDescriptor / detachClientFromDisk.

  • hypaware.plugin.json: drop attach_probe. contributes.client keeps
    name/skill_dir/agent_dir, the picker row and the install/verify
    commands are untouched.
  • src/index.js: the activate() JSDoc justified the client descriptor by
    "the plist is a real local surface", which is what led here. It now says why
    there is deliberately no probe, with @ref LLP 0115#no-attach-on-join.
  • llp/0135: corrected the bullet and added a {#no-probe} section recording
    all three reasons, so the next reader does not re-add it.

Behaviour after: detachClientFromDisk returns { changed: false } at its
existing no-probe guard, so hyp detach --client claude-desktop prints
nothing to do instead of erroring over a file core can neither read nor
reverse. Desktop's state surface stays hyp claude-desktop verify; its undo
stays removing the plist with sudo (@ref LLP 0133#one-surface). Attach on
join is unchanged and still inert: the plugin registers no runtime
ctx.clients adapter, so action_attach.js's desired() never named it.

The MALFORMED_MARKER guard is untouched. Refusing beats a half-reversal
and that is the documented intent (LLP 0045 Part 3); the bug was declaring a
probe over a file that guard was never meant to see.

Test

New test/plugins/claude-desktop-detach.test.js, binding to the shipped
manifest (not a hand-written copy) and to the plist install actually renders
via renderManagedPreferencesPlist:

  1. the bundled manifest declares no attach_probe;
  2. with the real rendered plist on disk, detachClientFromDisk returns
    changed:false and does not throw;
  3. the same plist never surfaces as a client attach-probe error in hyp status.

All three fail on origin/master (2 and 3 with the MALFORMED_JSON parse
error) and pass with this change.

npm test: 2795 pass, 8 fail - all 8 in test/core/leave-command.test.js,
verified identical on a pristine origin/master checkout (pre-existing,
unrelated).

Conflict note

No overlap with the held PR #441. This branch does not touch
src/core/config/client_detach_disk.js, src/core/config/types.d.ts, or
LLP 0045 at all. Files changed: the claude-desktop manifest, that plugin's
src/index.js, LLP 0135, and one new test file.

Follow-up worth its own issue (not fixed here)

resolveClientSettingsPath silently re-anchors an absolute settings_file
under $HOME rather than honouring or rejecting it. That is the trap that let
defect 2 above ship unnoticed, and it will catch the next manifest author too.

Fixes#444

`hyp detach --client claude-desktop` failed on exactly the machines where
`hyp claude-desktop install` had succeeded. The manifest declared a
`json_path` attach_probe over
`/Library/Managed Preferences/com.anthropic.claudefordesktop.plist`, and
core's probe/undo pair `JSON.parse`s the settings file, so detach raised
`MALFORMED_JSON` on the XML plist and `hyp status` surfaced the same parse
failure as a client probe `error`.
The probe was wrong three ways over, any one of them fatal:
- the file is XML, not JSON
- `settings_file` is $HOME-relative by contract, so the absolute
`/Library/...` path re-anchored to `~/Library/...` and never named the
real file
- the plist carries no `marker_record`, and per LLP 0045 Part 3 a
record-less marker is refused rather than half-reversed. Adding one
would not help either: the file is root-owned, so an unprivileged
detach cannot write it back
LLP 0115#no-attach-on-join (Accepted) had already settled this: Desktop
registers no attach_probe, because the LLP 0044 loop needs a reversible
settings-file write and the managed plist is not one. LLP 0135's design
bullet reintroduced one anyway, "reflecting the attribution finding" -
but an attach_probe is not a label, it is the input to
`probeClientAttachFromDescriptor` / `detachClientFromDisk`.
So: remove it. `contributes.client` keeps `skill_dir`/`agent_dir`, the
picker row and the install/verify commands are untouched, and attach on
join stays inert exactly as before (the plugin registers no runtime
`ctx.clients` adapter). `detachClientFromDisk` now returns
`{ changed: false }` at its no-probe guard, so the command is an honest
no-op instead of an error over a file core can neither read nor reverse.
Desktop's state surface remains `claude-desktop verify` and its undo
remains removing the plist with sudo.
No core code changes: the MALFORMED_MARKER guard is left exactly as it
is, since refusing beats a half-reversal.
Co-Authored-By: Claude <noreply@anthropic.com>
neutral-loopand others added 2 commits July 29, 2026 05:09
The new detach test built its plist fixture with buildManagedProfile({
baseUrl, authScheme, models, helperPath }), missing the required
bundleId field ProfileInputs declares, which failed typecheck (TS2345)
on both Node 22 and 24. Import DEFAULT_BUNDLE_ID from profile.js and
pass it through; the test's three assertions (no attach_probe on the
manifest, detach is a changed:false no-op, and the plist never
surfaces as an attach-probe error) are unchanged.
Co-Authored-By: Claude <noreply@anthropic.com>
…s stale ask
Two doc-accuracy defects on top of the (correct) attach_probe removal.
1. LLP 0135 {#no-probe} item 1 claimed `hyp detach --client claude-desktop`
raised MALFORMED_JSON "on a machine where install had succeeded", but
item 2 of the same list proves that impossible: `resolveClientSettingsPath`
re-anchors the absolute `/Library/Managed Preferences/...` under $HOME, and
`install` writes only the root-owned absolute path, so the probe never read
the plist at all. Defect 2 masked defect 1: the field symptom was a silent
wrong "not attached", and MALFORMED_JSON is what the probe produces once the
path is corrected (which is the state the new test reproduces). Reworded so
the section is internally consistent and so the reason for removing rather
than repairing the probe is explicit.
2. LLP 0136 T12 still instructed an implementer to declare "an `attach_probe`
reflecting the `entrypoint: claude-desktop-3p` attribution finding" - the
exact stale guidance this change set exists to retire. Corrected to record
no probe, with refs to LLP 0115#no-attach-on-join and LLP 0135#no-probe.
Also narrows the `@ref LLP 0115#no-attach-on-join` gloss in the plugin's
`activate()` JSDoc, which repeated the same overstated field claim.
No code or test behaviour changes.
Co-Authored-By: Claude <noreply@anthropic.com>
@philcunliffe

Copy link
Copy Markdown
ContributorAuthor

Verdict: approve the change, with two doc-accuracy findings (both fixed and pushed)

The central design question, does LLP 0135 legitimately supersede LLP 0115, resolves in this PR's favour. Removing the probe is the correct restoration, not a regression. Two findings, both about the rationale rather than the change: one left the new LLP section internally self-contradictory, and one left the exact stale instruction this change set exists to retire sitting in another LLP.


1. The LLP 0115 vs 0135 question, verified independently

docTypeStatussupersede metadata
llp/0115-claude-desktop-managed-config-attach.decision.mdDecisionAccepted (:4)none. No Superseded-by, no Supersedes. Not in llp/tombstones/.
llp/0133-desktop-solo-sudo-plist.decision.mdDecisionAccepted (:4)none; explicitly refuses to supersede 0115
llp/0135-install-experience-overhaul.design.mddesignActive (:5)none. 0115 appears only under **Related:** (:7)

llp/0115:98-102 says verbatim what the author claims:

<a id="no-attach-on-join"></a>Desktop is not an attach-on-join client. The plugin registers no contributes.client with an attach_probe: the LLP 0044 loop requires a reversible settings-file write that Desktop cannot offer.

And llp/0133:10-14 settles the authority question outright:

Also carries the live-test corrections to LLP 0115, which is Accepted and immutable; corrections land here, as new findings that ref it, not as edits to it.

0133 then lists exactly three corrections to 0115 (:36-63) and none touches #no-attach-on-join. llp/0133:38-40 says 0115's "decision (managed config, not the dialog) stands, reinforced". The attribution correction (llp/0133:58-63) is about where a captured row's client identity comes from (entrypoint: "claude-desktop-3p", not the User-Agent), a projection concern with no descriptor input at all.

So a design doc (0135), whose own header lists 0115 as merely Related, cannot and does not supersede an Accepted Decision that a later Accepted Decision reaffirms. The author's "category error" reading is right: attach_probe is consumed at src/core/daemon/status.js:957 and src/core/config/client_detach_disk.js:85 as the input to the probe/undo pair, never as a label. This PR is not backwards.

2. Nothing regresses. Verified from the code, not the PR body

  • src/core/config/action_attach.js:85-322 - desired() skips a descriptor unless it passes bothif (!descriptor.attachProbe) continue (:314) andif (!clients.getClient(descriptor.name)) continue (:316). The plugin registers no runtime ctx.clients adapter (grep over hypaware-core/plugins-workspace/claude-desktop/src/ returns only the comment saying so), so the second gate already excluded claude-desktop. Removing the probe changes nothing here. Attach-on-join was inert before and is inert now.
  • reverse()'s probe-less branch (action_attach.js:312-322) only fires when a marker exists, which requires an attach that could never have happened.
  • hyp detach --client claude-desktop -> detachClientViaCore (src/core/commands/clients.js:404) -> detachClientFromDisk, which returns { changed: false } at its no-probe guard (src/core/config/client_detach_disk.js:86-87). Honest no-op, and the marker-retraction gate at clients.js:455+ already handles the probe-less case.
  • hyp status -> probeClientAttachFromDescriptor returns { attached: false } on !descriptor.attachProbe (src/core/daemon/status.js:957). No error row.
  • The remaining shape is valid, not half-wired.attach_probe? is optional in the kernel contract (hypaware-plugin-kernel-types.d.ts:160), src/core/manifest.js accepts contributes opaquely apart from picker (:148-198), and src/core/plugin_catalog.js:77 sets attachProbe conditionally. skill_dir/agent_dir remain live inputs to src/core/runtime/client_assets.js:110,174-175. The picker row lives under contributes.picker with an app_bundle detect, untouched.
  • The attribution concern is not lost, because the probe never carried it. Attribution rides entrypoint per LLP 0133#attribution; the PR keeps that @ref on activate() and adds an explicit "None of these change attribution" paragraph at llp/0135:443-446.
  • npx tsc -p tsconfig.json --noEmit clean. npm test: 2795 pass / 8 fail, and node --test test/core/leave-command.test.js alone accounts for all 8. Baseline matches.

3. Test quality: genuinely red on master

Reverting only the manifest hunk to origin/master and rerunning:

not ok 1 - the claude-desktop manifest declares no attach_probe
not ok 2 - hyp detach --client claude-desktop is a no-op over the managed plist
not ok 3 - the managed plist never surfaces as a client attach-probe error
# pass 0 / # fail 3

All three bind to the shipped artefacts: the manifest is read from hypaware.plugin.json and the descriptor rebuilt the way plugin_catalog.js:70-78 does (test/plugins/claude-desktop-detach.test.js:42-56), and the plist comes from the plugin's own renderManagedPreferencesPlist(buildManagedProfile(...)) (:77-85). No hand-made fixture to drift.


Findings

Finding 1 - Medium - llp/0135:463-472 was self-contradictory about the field symptom (FIXED)

The new {#no-probe} section's item 1 asserted:

On a machine where install had succeeded, hyp detach --client claude-desktop therefore raised MALFORMED_JSON and hyp status surfaced the parse failure as a client probe error.

Item 2 of the same list proves that cannot have happened. resolveClientSettingsPath is path.join(homeDir, ...settingsFile.split('/')) (src/core/daemon/client_settings_path.js:21-28), so the manifest's absolute /Library/Managed Preferences/... resolved to ~/Library/Managed Preferences/.... install writes only the absolute root-owned path (hypaware-core/plugins-workspace/claude-desktop/src/install.js:22, and :73-78 via sudo cp + chmod 644), and nothing in the tree ever writes the home-relative one. Defect 2 masked defect 1: a configured machine got a silent wrong "not attached", not a parse error. MALFORMED_JSON is what the probe produces once the path is corrected, which is precisely the state the new test manufactures by writing the plist to the re-anchored path (test/plugins/claude-desktop-detach.test.js:58-73, whose helper JSDoc is honest about this).

This matters because the section exists so "the next reader does not re-add it", and as written that reader would hit two mutually exclusive claims a paragraph apart, then go looking for a MALFORMED_JSON report that no user could have filed. The same overstatement was repeated in the @ref gloss at hypaware-core/plugins-workspace/claude-desktop/src/index.js:60.

Fixed in 65e8a31: item 1 now states the throw as a property of the probe over the rendered plist rather than as an observed field event; item 2 gains the masking interaction explicitly; and a closing paragraph records why the probe is removed rather than path-corrected (fixing the path alone upgrades a quiet wrong answer to a loud one, and defects 1 and 3 are unfixable inside the attach_probe contract). The index.js:60 gloss is narrowed to match.

This does not weaken the case for the change, it arguably strengthens it. A probe that silently reports "not attached" for a reason unrelated to the truth is the worse of the two failure modes, and it is the one that actually shipped.

Finding 2 - Medium - llp/0136:74 still told the next implementer to add the probe back (FIXED)

llp/0136-install-experience-overhaul.plan.md (Type: plan, Status: Implemented) task T12 still read:

hypaware.plugin.json declares contributes.client (name: "claude-desktop", skill_dir/agent_dir, an attach_probe reflecting the entrypoint: "claude-desktop-3p" attribution finding per @ref LLP 0133#attribution)

This is the verbatim ancestor of the bullet the PR corrected in 0135, and it survived untouched. The PR's stated goal is that "the next reader does not re-add it"; anyone working the plan doc, or diffing the implementation against it, would have re-added it. CLAUDE.md is explicit: "don't leave stale guidance."

Fixed in 65e8a31: T12 now records no probe, names the category error, and refs LLP 0115#no-attach-on-join [constrained-by] and LLP 0135#no-probe.

Finding 3 - Low - not fixed, and correctly out of scope

resolveClientSettingsPath (src/core/daemon/client_settings_path.js:21-28) silently re-anchors an absolute settings_file under $HOME rather than honouring or rejecting it. This is the root cause that let the broken probe ship unnoticed, and it will catch the next manifest author identically. The PR body already flags it as deserving its own issue and correctly does not touch it here (it is shared core, and touching it would widen the blast radius of a manifest-only fix). No follow-up issue exists yet, worth filing. I added a parenthetical pointer at the end of llp/0135#no-probe so the trap is at least recorded next to its one known victim.

Non-findings, checked

  • @ref anchors all resolve: llp/0115<a id="no-attach-on-join"> (:98), llp/0133#attribution (:58) and #one-surface (:18), the new ### No attach probe for Desktop {#no-probe} (llp/0135:448, and {#slug} heading anchors are the corpus convention, see llp/0027:100, llp/0077:30, llp/0040:139), and llp/0045's #part-3--reverse-runs-from-disk-... (:197).
  • No em dash introduced anywhere in the diff. No semicolons in the new test. JSDoc types only, @import at top of file, root-anchored .js specifiers.
  • The MALFORMED_MARKER guard (client_detach_disk.js:411) is untouched, as claimed, and the author's analysis that it was unreachable for this client is correct: detachJsonPathMarker returns early at :399 on !isPlainObject(marker), and inferenceCredentialHelper is a plain string per llp/0115:120-129 (#real-profile-keys).
  • No conflict with the held PR detach: report every externally-overridden managed key, not just the last (issue #440 finding 2) #441. This branch does not touch client_detach_disk.js, src/core/config/types.d.ts, or LLP 0045.

Pushed to fix/issue-444: d524e9c..65e8a31. Docs and one JSDoc gloss only, no code or test behaviour change. Typecheck clean, the three new tests still pass, suite still at the 8 known leave-command failures.

Round 1 verified the {#no-probe} rationale itself is now accurate (install
writes only the root-owned absolute /Library/Managed Preferences path, so
the $HOME-re-anchored path the probe read never existed), but the section
sits mid-way through `## hyp claude-desktop install`, so the six-step
`src/install.js` spec and its idempotency paragraph rendered as content of
a `###` subsection titled "No attach probe for Desktop" rather than of the
parent section they document.
Move the subsection to the end of the parent section. The forward link at
"See [no probe for Desktop](#no-probe) below" still resolves, the anchor is
unchanged, and no prose was reworded.
Also names the tracking issue for the `resolveClientSettingsPath`
absolute-path trap (#446) so "tracked on its own" is checkable.
Co-Authored-By: Claude <noreply@anthropic.com>
@philcunliffe

Copy link
Copy Markdown
ContributorAuthor

Round 2 (final) — verdict: approve, with one LOW doc-structure fix pushed

Round 1's two doc corrections hold up. One structural defect in llp/0135 (present since 2f34c52, not introduced by round 1) is fixed here. Nothing remains for triage.


1. Round 1's rewritten causal claim is TRUE — verified from code, not prose

llp/0135 now says defect 2 ($HOME re-anchoring) masked defect 1 (XML vs JSON.parse), so the field symptom was a silent wrong "not attached", not MALFORMED_JSON. Each link checked independently:

  • Install writes only the root-owned absolute path.install.js:22MANAGED_PLIST_PATH = '/Library/Managed Preferences/com.anthropic.claudefordesktop.plist'; install.js:105opts.managedPlistPath ?? MANAGED_PLIST_PATH, and the managedPlistPath override has no production caller — every use is in test/plugins/claude-desktop-install.test.js / claude-desktop-verify.test.js. The write itself is sudo cp (install.js:73-77).
  • The only $HOME path the plugin touches is a different one.install.js:36-37~/Library/Application Support/Claude/Claude-3p (the residue dir). Nothing writes ~/Library/Managed Preferences/.
  • The probe re-anchored under $HOME.client_settings_path.js:28path.join(homeDir, ...settingsFile.split('/')); the leading empty segment of an absolute path is dropped by path.join, so /Library/Managed Preferences/x.plist$HOME/Library/Managed Preferences/x.plist. The CLAUDE_DESKTOP_HOME override branch (:22-27) is not set in practice.
  • A missing file is silent, not an error.status.js:1012-1013 returns { attached: false, settingsPath } on ENOENT, with no error field. So a configured machine reported "not attached" and hyp status showed no probe error.
  • And the loud failure is real once the path is right. Ran the new test against a pristine origin/master worktree: all 3 assertions fail, [codex] Remove OpenTelemetry npm dependencies #2 with code: 'MALFORMED_JSON' / Unexpected token '<', "<?xml vers".... That is exactly the "path corrected" state the test deliberately reproduces by placing the plist at the re-anchored location.

Conclusion: the causal chain in {#no-probe} item 2 is accurate, self-consistent with item 1, and does not over-claim in the other direction. hyp status's error path and the probe's silent path are correctly distinguished.

2. llp/0136's task block still parses — confirmed against the real parser

Ran /opt/neutral/src/tasks.js's parseTasks (regex TASK_LINE at tasks.js:11) over llp/0136 at 65e8a31:

count 13
T12 → branch: task/install-experience-overhaul/T12, deps: ["T1"], complexity: 2, brief: 1339 chars

All four required fields survive. T12 contains exactly one -- separator (checked), so the brief split is unambiguous despite the added prose. No duplicate-id or dangling-dep error. Block is intact.


Findings

LOW — llp/0135:448 (at 65e8a31): the ### No attach probe for Desktop {#no-probe} subsection was inserted mid-section, orphaning the install spec under it.## hyp claude-desktop install {#claude-desktop} starts at :425; the new ### landed at :448, and the next ## is not until :526. Everything between — the six-step src/install.jsrun(argv, ctx) spec (:499-518, i.e. T13's whole contract, incl. @ref LLP 0133#solo-sudo, #plist-surface, #dialog-residue, #verify-is-a-hint) and the idempotency/ephemeral-listen paragraph (:519-524) — therefore rendered as content of a subsection titled "No attach probe for Desktop", which is not what it documents. Pre-existing from 2f34c52, missed in round 1.

NIT — llp/0135:490: "tracked on its own" named no tracker. Issue #446 now exists (resolveClientSettingsPath silently re-anchors an absolute settings_file under $HOME instead of rejecting it, OPEN), so the claim can be made checkable.

Fixed in 2c50890 (pushed to fix/issue-444)

  • Moved the {#no-probe} block to the end of ## hyp claude-desktop install, restoring the install spec to parent-section level. Verified: at 65e8a31 the order was 448:### No attach probe499:src/install.js526:## Export-seam; at 2c50890 it is 448:src/install.js475:### No attach probe526:## Export-seam. Anchor {#no-probe} unchanged, the forward link at :435 ("See no probe for Desktop below") still resolves, no prose reworded.
  • :490 now reads "tracked on its own as resolveClientSettingsPath silently re-anchors an absolute settings_file under $HOME instead of rejecting it #446". Verified present in the committed tree.
  • git diff 65e8a31 HEAD --stat touches llp/0135 only — llp/0136, the manifest, src/index.js and the test are byte-identical to the reviewed head.

Also checked, no finding

  • No behavioural regression from dropping the probe.action_attach.js:314,316desired() requires both an attachProbeand a ctx.clients entry; the plugin registers no runtime clients adapter, so claude-desktop was already excluded. action_attach.js:312reverse() and clients.js:501,528 gate marker-retraction and org-asset removal on attachProbe — with no probe both are skipped, which is correct precisely because no attach marker can ever exist for this client (no desired() entry ⇒ no marker ⇒ readInstalledAssets has nothing to strand).
  • hyp detach --client claude-desktop output.client_detach_disk.js:86 no-probe guard → { changed: false }writeCoreDetachOutput (clients.js:597-600) prints No HypAware marker found; nothing to do. Matches the PR body.
  • Picker detection unaffected.detect.js:75-80 probes the contributes.pickerdetect key (app_bundle: /Applications/Claude.app), never attach_probe.
  • Manifest shape still valid.attach_probe? is optional (hypaware-plugin-kernel-types.d.ts:160); plugin_catalog.js:77 only sets descriptor.attachProbe when present.
  • @ref anchors all resolve.LLP 0115#no-attach-on-join (0115:98), LLP 0133#attribution (0133:58), #one-surface (0133:18), #0115-corrections (0133:38), LLP 0135#no-probe (0135:475), and 0045's #part-3--... slug (0045:197, same slug already used at client_detach_disk.js:35).
  • Design status re-confirmed. LLP 0115 Accepted with no supersede metadata; LLP 0133 Accepted; LLP 0135 Active; LLP 0136 Implemented. Removing the probe restores 0115, it does not overturn anything.
  • Gates.npm run typecheck clean. npm test at 65e8a31: 2795 pass / 8 fail, and node --test test/core/leave-command.test.js alone yields exactly 3 pass / 8 fail — the whole delta is the documented pre-existing baseline. New test: 3/3 pass at head, 0/3 on origin/master. No em dash anywhere in the diff; no semicolons in the new JS.

For triage

Nothing. One observation, deliberately not acted on (the PR body is not mine to edit): the PR body and commit 2f34c52's message still carry the pre-round-1 wording that the field symptom was MALFORMED_JSON "on exactly the machines where install had succeeded". llp/0135 — the durable artifact — now states this correctly, and the body's own reproduction table is honest about which on-disk state produces which result, so this is cosmetic drift in an ephemeral surface rather than a defect. Worth a one-line body edit by the author before ready-for-review.

@philcunliffe

Copy link
Copy Markdown
ContributorAuthor

Triage verdict: no true blocker. Classification: PREFERENCE (stale doc wording only).

Both recorded review rounds (d524e9c, 65e8a31) fixed everything they found in-round; round 2's verdict was Approve with nothing deferred. I independently re-verified the one residual round 2 flagged as non-blocking and cosmetic, rather than trusting the summary:

  • resolveClientSettingsPath (src/core/daemon/client_settings_path.js:21-29) always re-anchors settings_file under $HOME, even when the manifest value is an absolute path. Confirmed by reading the function directly.
  • The real managed plist is written by hyp claude-desktop install to the system path MANAGED_PLIST_PATH = '/Library/Managed Preferences/com.anthropic.claudefordesktop.plist' (hypaware-core/plugins-workspace/claude-desktop/src/install.js:22), never under $HOME.
  • probeClientAttachFromDescriptor (src/core/daemon/status.js:1011-1013) treats ENOENT at the (wrongly re-anchored) path as { attached: false } with no error field.
  • So the real field symptom on a machine where install succeeded was a silent, wrong "not attached", not MALFORMED_JSON. I confirmed this empirically too: running the PR's own new test (test/plugins/claude-desktop-detach.test.js) against a fresh origin/master worktree reproduces MALFORMED_JSON only because the test deliberately places the rendered plist at the re-anchored path - which is not where install actually writes it in the field.
  • llp/0135#no-probe (lines ~495-505 of llp/0135-install-experience-overhaul.design.md) already states this correctly, including the masking interaction between the two defects. The PR body and commit 2f34c52's message still carried the pre-round-1, backwards claim.

Nothing else looked wrong: the manifest/index.js diff is a clean, minimal attach_probe removal with no other behavior change, llp/0136's T12 edit is a single corrected line with T13 intact, and the new test passes at head 2c50890 and fails (as claimed) on origin/master.

Action taken: corrected the stale MALFORMED_JSON-on-successful-install wording in the PR body to the accurate silent-"not-attached" account, preserving the rest of the body verbatim (including the reproduction table, which is itself accurate as a description of what the test constructs). No new issue filed: the one real underlying defect (resolveClientSettingsPath silently re-anchoring an absolute settings_file) is already tracked, open, as #446 - filing a second one would be issue spam.

Not touching draft/ready state or merging. Marker to follow as a separate edit so a partial failure re-triages rather than being skipped.

@philcunliffe
philcunliffe marked this pull request as ready for review July 29, 2026 05:32
@philcunliffephilcunliffe added the neutral:approved neutral reviewed this and holds it for a maintainer merge (own or adopted PR; LLP 0025/0030) label Jul 29, 2026
philcunliffe pushed a commit that referenced this pull request Jul 29, 2026
… not the first character
The isAbsolute guard rejects '/etc/passwd' and lets '../../../etc/passwd'
through to exactly the same place. Both land on a file the manifest never
named, which is the whole harm this guard exists to stop, and the escaping
spelling is the one that survives the check:
resolveClientSettingsPath('codex', '../../../etc/passwd', {}, '/Users/hyp')
-> '/etc/passwd' (pre-fix)
.codex/../../../etc/passwd + CODEX_HOME=/tmp/ch -> '/etc/passwd'
It matters more here than in a read-only resolver: this is also the write
side, detachClientFromDisk reads and rewrites whatever it is handed, and
contributes.client is unvalidated, so the value can arrive from a
remotely-installed or org-pushed plugin.
Require the resolved path to stay under the base it resolved against
(settings_file_escapes_base). Each branch checks its own base - $HOME
normally, $<CLIENT>_HOME when set, since the override is exactly a licence
to leave $HOME. A '..' that normalizes away stays legal.
Also drops two overclaims the section had picked up: attach_probe really
does lose expressiveness (it has no absolute sibling, which is why #445
deletes Desktop's probe rather than respelling it), and the one-resolver
argument holds for core but not for the per-plugin attach writers - the
claude plugin hardcodes ~/.claude/settings.json and ignores $CLAUDE_HOME.
And the ClientSettingsPathError JSDoc no longer claims callers avoid
message-text matching when its only consumer flattens to a message.
philcunliffe pushed a commit that referenced this pull request Jul 29, 2026
…o it
The errored `claude-desktop` row was pushed onto whatever
`collectHypAwareStatus` had already produced, and the bundled catalog
produces a `claude-desktop` row of its own carrying this very error - the
manifest defect #445 removes. So the list held two same-named rows and
the JSON assertion's `find` answered from the catalog's, passing for the
wrong reason.
It stops passing the moment #445 lands: the catalog row loses its error,
`find` still returns it first, and `the JSON renderer carries the same
client error` fails. Verified both ways - `git merge origin/fix/issue-444`
into this branch failed that test before this commit and passes after,
with the 8 known leave-command failures unchanged in both trees.
Replace the same-named row rather than joining it. These tests are about
the two renderers, so they own the whole client list and assert nothing
about which clients the bundled catalog happens to ship.
Co-Authored-By: Claude <noreply@anthropic.com>
philcunliffe added a commit that referenced this pull request Jul 29, 2026
…f re-anchoring it under $HOME (#447)
* resolveClientSettingsPath rejects an absolute settings_file instead of re-anchoring it under $HOME
An absolute `settings_file` was not an error: `path.join(homeDir,
...settingsFile.split('/'))` swallows the leading empty segment, so
`/Library/Managed Preferences/com.anthropic.claudefordesktop.plist`
silently became `$HOME/Library/Managed Preferences/...`. The env-override
branch was wrong the same way (`parts.slice(1)` assumes a relative first
segment, so it dropped the leading `/` and grafted the rest onto
`$<CLIENT>_HOME`).
The probe then answered about a file the manifest never named. The usual
answer was ENOENT, which reads exactly like a correct "not attached", and
that wrong-negative-indistinguishable-from-a-right-one is how the Claude
Desktop attach_probe defect (#444) stayed invisible. It can also be a
wrong positive: the new probe test builds the marked decoy at the
re-anchored `$HOME` location and the old code reports `attached: true`.
Reject rather than honour: the `$<CLIENT>_HOME` override relocates a
config home, which an absolute path does not have, so honouring would
publish a second silently-different resolution rule for one field. The
resolver is shared by the read side (attach probe, picker detect) and the
write side (the disk-driven undo), so a value core cannot resolve must
fail rather than resolve to something else.
Each caller renders the throw on its own terms: the status probe returns
`{ attached: false, error }` (already carried through to `hyp status`),
`hyp detach` fails loudly, and the init picker keeps its documented
best-effort "not present". Load-time manifest validation is left to
follow-up, sequenced after the Desktop manifest is corrected.
LLP 0045 Part 3 gains the governing section, and the kernel type now
states the home-relative contract it only ever implied.
Co-Authored-By: Claude <noreply@anthropic.com>
* hyp status: print a client probe error on the text surface, not only under --json
The guard makes an unresolvable settings_file an `error` on the probe
result, but `renderStatusText` dropped the field: it printed the client
as a bare `not attached` and, because such a client is typically not
`configured`, usually collapsed the whole section into `clients: (none)`.
On stock master + this branch that is exactly what happened to
claude-desktop, so the wrong negative the PR set out to end survived one
layer up, on the surface a human actually reads. --json already carried it.
Print the message under the client's line, and keep an errored client out
of the `(none)` collapse. LLP 0045 Part 3 records that both surfaces carry
it.
* resolveClientSettingsPath: enforce the contract on the resolved path, not the first character
The isAbsolute guard rejects '/etc/passwd' and lets '../../../etc/passwd'
through to exactly the same place. Both land on a file the manifest never
named, which is the whole harm this guard exists to stop, and the escaping
spelling is the one that survives the check:
resolveClientSettingsPath('codex', '../../../etc/passwd', {}, '/Users/hyp')
-> '/etc/passwd' (pre-fix)
.codex/../../../etc/passwd + CODEX_HOME=/tmp/ch -> '/etc/passwd'
It matters more here than in a read-only resolver: this is also the write
side, detachClientFromDisk reads and rewrites whatever it is handed, and
contributes.client is unvalidated, so the value can arrive from a
remotely-installed or org-pushed plugin.
Require the resolved path to stay under the base it resolved against
(settings_file_escapes_base). Each branch checks its own base - $HOME
normally, $<CLIENT>_HOME when set, since the override is exactly a licence
to leave $HOME. A '..' that normalizes away stays legal.
Also drops two overclaims the section had picked up: attach_probe really
does lose expressiveness (it has no absolute sibling, which is why #445
deletes Desktop's probe rather than respelling it), and the one-resolver
argument holds for core but not for the per-plugin attach writers - the
claude plugin hardcodes ~/.claude/settings.json and ignores $CLAUDE_HOME.
And the ClientSettingsPathError JSDoc no longer claims callers avoid
message-text matching when its only consumer flattens to a message.
* client_settings_path: return the path the guard checked, and pin the prefix test
Round-2 review of the containment guard added in 623eb6b. The guard holds
against every escape spelling I could build (absolute, `..` in either
branch, prefix-sharing siblings, `..` landing on the base), but three
things about it were either untested or unwritten:
- The `base + separator` suffix in the prefix test is the entire check -
a bare `startsWith(base)` accepts `/home/username` under a `/home/u`
base - and deleting the suffix made no test fail. Pinned.
- `withinBase` validated `path.resolve(joined)` and returned the raw
`joined`. They diverge whenever the base is relative: `CODEX_HOME=..`
returned the relative `../config.toml`, i.e. a value re-resolved
against `process.cwd()` at read time rather than the one validated at
call time, and not the absolute path the resolver's contract promises.
Return the checked path.
- The check is lexical, not realpath-based, so a config home that is
itself a symlink out of $HOME still passes. That is the right
boundary - the field is resolved before the file must exist, and
planting such a symlink already needs write access to $HOME, whereas
the untrusted input is the unvalidated manifest value - but it was
nowhere stated, and an undocumented limit on a security control
invites over-trust. Written down in the JSDoc and in LLP 0045 Part 3.
Co-Authored-By: Claude <noreply@anthropic.com>
* status-client-error tests: own the client list instead of appending to it
The errored `claude-desktop` row was pushed onto whatever
`collectHypAwareStatus` had already produced, and the bundled catalog
produces a `claude-desktop` row of its own carrying this very error - the
manifest defect #445 removes. So the list held two same-named rows and
the JSON assertion's `find` answered from the catalog's, passing for the
wrong reason.
It stops passing the moment #445 lands: the catalog row loses its error,
`find` still returns it first, and `the JSON renderer carries the same
client error` fails. Verified both ways - `git merge origin/fix/issue-444`
into this branch failed that test before this commit and passes after,
with the 8 known leave-command failures unchanged in both trees.
Replace the same-named row rather than joining it. These tests are about
the two renderers, so they own the whole client list and assert nothing
about which clients the bundled catalog happens to ship.
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: neutral-loop <neutral-loop@users.noreply.github.com>
Co-authored-by: Claude <noreply@anthropic.com>
@philcunliffe
philcunliffe merged commit 74aea66 into masterJul 29, 2026
8 checks passed
@philcunliffe
philcunliffe deleted the fix/issue-444 branch July 29, 2026 18:58
philcunliffe pushed a commit that referenced this pull request Jul 29, 2026
Two resolutions, one textual and one semantic. Neither reverses a landed
decision.
1. `claude-desktop/hypaware.plugin.json` (the only git-reported conflict).
#445 (`74aea66`) deleted the `attach_probe` block from
`contributes.client`, restoring the Accepted decision at
LLP 0115#no-attach-on-join and LLP 0135#no-probe. This branch added
`transcript_entrypoints` as a sibling of that block, so the two edits
collided on the same closing brace and nothing more. Took master's
deletion whole and kept only the new field:
"agent_dir": ".claude/agents",
"transcript_entrypoints": ["claude-desktop", "claude-desktop-3p"]
The probe stays gone. `transcript_entrypoints` is a backfill-ownership
label read by `resolveOwnersForRun` (LLP 0140), never by
`probeClientAttachFromDescriptor` or `detachClientFromDisk`, so it does
not reintroduce an attach-on-join path and `test/plugins/
claude-desktop-detach.test.js`'s no-probe assertion still holds.
2. `test/core/compose-picker-config.test.js` (a silent semantic conflict
git auto-merged). #433 (`8eccc04`) stopped `composePickerConfig` pinning
the gateway `listen` so LLP 0114's fixed default applies to a
wizard-created install. The `claude-desktop composes ...` case added
here predates that and still expected
`listen: '127.0.0.1:8787'`, so it was the one gateway slice in the file
carrying a `listen` and it failed on the merge. Dropped the pin to match
master's landed decision and the other eleven cases; the assertion this
test exists for, that the Desktop row composes both plugins its
`configure_command` needs, is untouched.
`src/core/daemon/status.js` merged clean and reads coherently: master's
no-probe guard (`descriptor.attachProbe ? probe : { attached: false }`)
sits directly above this branch's `client_attach_missing` repair fix. Post
#445 that fix matters more, not less: with no probe at all, a configured
claude-desktop always reports `attached: false`, so the repair line is
always printed, and the generic `hyp attach --client claude-desktop` it
used to print answers `unknown client`.
npm test: 2873 pass / 8 fail, exactly the pre-existing
test/core/leave-command.test.js set. npm run typecheck clean. Smokes
walkthrough_picker_to_first_query, cli_bundled_plugins_activated,
status_diagnostics ok.
Co-Authored-By: Claude <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

neutral:approvedneutral reviewed this and holds it for a maintainer merge (own or adopted PR; LLP 0025/0030)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

hyp detach --client claude-desktop always fails with MALFORMED_MARKER

1 participant

@philcunliffe