Skip to content

detach: the undo replays every backup the marker holds, and says what it restored (#500) - #506

Merged
philcunliffe merged 4 commits into
masterfrom
fix/issue-500
Jul 31, 2026
Merged

detach: the undo replays every backup the marker holds, and says what it restored (#500)#506
philcunliffe merged 4 commits into
masterfrom
fix/issue-500

Conversation

@philcunliffe

@philcunliffe philcunliffe commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

Deferred review findings from #495 (LLP 0163: claude attach backs a malformed
env/hooks block up onto the _hypaware marker and repairs it, rather than
refusing). #495 has merged, so these are actionable. All three fixed findings
are the undo failing to say something, two of them while dropping a backup
and reporting a successful detach.

No maintainer decision was recorded in the issue thread (gh issue view 500 --json comments returns an empty list), so nothing here overrides the filed
findings.

Finding 3 - a successful prev_malformed restore was completely silent

Verified on master: a detach that puts the user's env block back sets none
of removed / restoredValue / warning, so hyp detach prints
✓ Detached claude and nothing else while rewriting the file.

DetachFromDiskResult gains restoredPaths?: string[]. hyp detach prints
Restored <path> from the marker's malformed-block backup per entry and
echoes restored_paths into --json.

Paths, never values. LLP 0163 already forbids the attach-side notices from
echoing the displaced value (a malformed env block is exactly where an API key
ends up, and these strings are printed and logged); the same rule governs the
way back. That is also why this cannot ride on restoredValue, which both
consumers render as a bare scalar. A list rather than a joined string, for the
reason LLP 0045 Part 3 already gives for warnings: string[].

FAIL on master:

not ok 34 - #500 finding 3: a restored malformed-block backup is reported by path, not silently
not ok 40 - #500 finding 3: `hyp detach` prints the restored block, and never its contents

PASS after:

ok 34 - #500 finding 3: a restored malformed-block backup is reported by path, not silently
ok 40 - #500 finding 3: `hyp detach` prints the restored block, and never its contents

Finding 2 - the delete-then-detach resurrection: behaviour deliberately unchanged

Reproduced on master: hand-delete the repaired env block after attach, and
hyp detach puts the original malformed value back, where the sibling
prev_base_url leaves a hand-deleted leaf deleted.

Not changed. Two reviewers and triage judged the restore defensible (at the
moment of deletion the block held only hypaware's own repaired keys, so
restoring the pre-attach value completes a partial manual detach and nothing is
lost), and flipping it would discard a value rather than keep one. It does not
meet the blocker bar: nothing is made unrecoverable while the user is told it is
safe.

What is fixed is that it happened without a word. Finding 3's field covers it,
and the regression test is written in exactly that shape:

FAIL on master / PASS after:

not ok 35 - #500 finding 2: the delete-then-detach resurrection is announced, not silent
ok 35 - #500 finding 2: the delete-then-detach resurrection is announced, not silent

Finding 1 - the legacy branch dropped backups silently

detachLegacyJsonMarker is selected by marker.managed not being a plain
object, so a current-shape marker whose record has been damaged lands there
still carrying prev_malformed / prev_base_url. Those were dropped without a
word, while the marker holding the only copy was deleted in the same write. On
this document's own terms that is destruction reported as a successful detach.

The branch now:

  • replays prev_malformed through the same helper and the same words as the
    record-driven branch (extracted as replayPrevMalformed), reporting a backup
    it cannot put back rather than dropping it;
  • restores a recorded prev_base_url instead of deleting the key;
  • strips classify-cwd hooks as well as session-context ones - matching
    hyp claude-hook … is proof of ownership, not a guess, so widening the
    pattern cannot clobber a user's own hook;
  • leaves the managed env keys the unreadable record named
    (ENABLE_TOOL_SEARCH, _CLAUDE_CODE_ASSUME_FIRST_PARTY_BASE_URL) in place
    and reports the reversal as partial. Nothing on disk distinguishes a value
    hypaware wrote from one the user did, and never-clobber-a-user-edit outranks
    tidiness. This is the one half of the finding that is reported rather than
    repaired, deliberately.

Reachability. As triage established by execution, ordinary attach() always
writes managed in the same write that ever sets prev_malformed, so this
shape is unreachable without a hand-edited (or otherwise corrupted) marker. The
fixtures therefore construct the state directly - breakManagedRecord() deletes
_hypaware.managed from a real post-attach file - and say so in a comment.

A genuine pre-record marker carries none of the triggering fields, so its
reversal and its silence are unchanged; that is pinned by its own test, which
passes on both sides.

FAIL on master:

not ok 36 - #500 finding 1: the legacy branch replays a prev_malformed backup instead of dropping it
not ok 37 - #500 finding 1: the legacy branch reports a prev_malformed backup it cannot put back
not ok 38 - #500 finding 1: the legacy branch restores prev_base_url rather than deleting the key
ok 39 - #500 finding 1: a GENUINE pre-record marker is reversed exactly as before, and says nothing new

PASS after:

ok 36 - #500 finding 1: the legacy branch replays a prev_malformed backup instead of dropping it
ok 37 - #500 finding 1: the legacy branch reports a prev_malformed backup it cannot put back
ok 38 - #500 finding 1: the legacy branch restores prev_base_url rather than deleting the key
ok 39 - #500 finding 1: a GENUINE pre-record marker is reversed exactly as before, and says nothing new

prev_base_url in this branch is not one of the four filed findings. It is the
identical defect one field over, in the same branch, exposed by the same fixture

  • a backup on the marker, silently discarded, from a detach reporting success -
    so fixing prev_malformed alone would have left the branch half honest.

Left open

  • Finding 4a, the sidecar backup file. A maintainer-parked open question in
    LLP 0163 (settings.json.hypaware-backup-<ts> would make the promise
    unconditional at the cost of on-disk surface nobody cleans up). Untouched.
    Note this change makes an unrestorable backup loudly discarded on both
    branches rather than silently on one, which is a reason to revisit it, not a
    resolution of it.
  • Finding 4b, a marker schema change for prior-wins between nested paths.
    The other parked open question. Untouched; the shallowest-first tiebreak and
    both recording sequences stay pinned by their existing tests.

Docs

LLP 0163 updated in the same commit: two new subsections under Detach restores
the backup
(the reporting rule, and the legacy branch), two consequences
bullets, and the paragraph that accepted the silent drop as corrupt-input
behaviour is replaced. Both open questions are left standing. @ref anchors
checked; the new legacy-branch annotation points at the new subsection.

Checks

  • npm test on this branch: 3094 tests, 3085 pass, 8 fail, 1 skipped.
  • Own origin/master baseline in a pristine worktree, run serially: 3087
    tests, 3078 pass, 8 fail, 1 skipped
    . The 8 are the pre-existing
    test/core/leave-command.test.js failures, identical by name on both sides
    (leave after join removes the seed and reports the server, leave clears an applied central slot, not just the seed, leave reverses org-driven attaches and drops the forward identity, leave after join also warns about a local central sink that keeps forwarding, leave is idempotent: a second leave is the not-connected no-op, leave still tears down when only a stale attach marker survives a prior partial leave, leave removes the assets its attach marker records, and leaves manual copies alone, leave self-heals an org attach whose plugin is gone: drops the marker, warns, stays clean). Delta is
    +7 tests, +7 passes.
  • npm run typecheck: clean.
  • npm run smoke -- client_attach_idempotent: ok.
  • npm run smoke -- claude_attach_detach: FAIL on both sides, at the same
    hook-command path assertion (the smoke expects the packaged hyp bin and gets
    the worktree's hypaware-core/smoke/index.js). Worktree artifact, disregarded.
  • npm run smoke -- client_attach_on_join: FAIL on both sides, at no re-attach: the attach.claude marker timestamp is unchanged. Pre-existing,
    unrelated.

No file here is touched by #491 (fix/issue-481) or #502 (fix/issue-421);
their live file lists were checked with gh pr diff --name-only.

Fixes #500

neutral and others added 3 commits July 31, 2026 02:40
… it restored (#500)

Three deferred findings from the review of #495 (LLP 0163, `claude attach`
backs a malformed `env`/`hooks` block up onto the marker and repairs it).
All three are the undo failing to say something, two of them while dropping
a backup and reporting success.

Finding 3 (silence). A successful `prev_malformed` restore set none of
`removed`/`restoredValue`/`warning`, so `hyp detach` printed `✓ Detached
claude` and stopped while it rewrote a block. `DetachFromDiskResult` gains
`restoredPaths: string[]`; the command prints one line each and echoes
`restored_paths` into `--json`. Paths, never values: a malformed `env` is
exactly where an API key ends up, which is also why this cannot ride on
`restoredValue` (rendered as a bare value by both consumers).

Finding 2 (delete-then-detach resurrection) is deliberately NOT changed.
Two reviewers and triage judged restoring the pre-attach value defensible,
and reversing it would discard a value rather than keep one. It is now
audible instead of silent, which was the objectionable half.

Finding 1 (the legacy branch). `detachLegacyJsonMarker` is selected by
`managed` not being a plain object, so a current-shape marker whose record
has been damaged lands there still carrying its backups - and they were
dropped without a word, while the marker holding the only copy was deleted
in the same write. It now replays `prev_malformed` through the same helper
and with the same words as the record-driven branch, and restores a
recorded `prev_base_url` instead of deleting the key. The managed env keys
the unreadable record named stay (nothing on disk proves they are ours, and
never-clobber outranks tidiness) and the reversal reports itself partial;
managed hooks are stripped, because matching `hyp claude-hook …` is proof
of ownership rather than a guess. A genuine pre-record marker carries none
of the triggering fields and is reversed, and stays silent, exactly as
before.

The shape finding 1 needs is only reachable by hand-editing `managed` out
of a marker, so the fixtures construct it directly.

Findings 4a (sidecar backup file) and 4b (a marker schema change for
prior-wins between nested paths) are the maintainer's parked open
questions and are left open.

LLP 0163 updated in the same commit.

Co-Authored-By: Claude <noreply@anthropic.com>
CI intermittently failed test/core/client-detach-disk.test.js: 3 assertions
did JSON.stringify(result).includes('sk-x') / out.includes('sk-x') to prove
the ANTHROPIC_API_KEY test secret never leaks into the detach result or CLI
output. But settingsPath is always present in that text, comes from
stageHome()'s fs.mkdtemp('.../hyp-detach-disk-'), and its random suffix
starts with 'x' about 1/62 of the time - which, glued onto the fixed prefix's
trailing "...disk-", spells "sk-x" with no secret involved. Strip settingsPath
out of the checked text before the leak check in all three assertions.

Reproduced deterministically by forcing a home dir suffix that starts with
'x' and confirming the false positive, then confirming the fix clears it.
Three review findings on #506.

1. `hyp detach --json` gained a `restored_paths` key but `ClientResult`
   (src/core/cli/types.d.ts) - the declared shape of that payload, and the
   return type of the published `integration.detach()` helper - was not
   extended, so a typed consumer sees a field the CLI emits but the contract
   denies. Declared, with the paths-never-values rule stated where a caller
   reads it.

2. The `--json` half of the new reporting had no test; only the prose line
   did. Added the sibling `--json` case, asserting `restored_paths` and the
   same no-secret-in-output check.

3. src/core/config/client_detach_disk.js imported `DetachFromDiskResult` as
   `from './types.d.ts'` - the only such specifier left in src/ (299 use the
   root-anchored form) and one CLAUDE.md forbids by name. It dangles in the
   published declaration build: tsc does not copy `.d.ts` inputs, so there is
   no `types/core/config/types.d.ts` for it to reach, and the interface this
   PR extends resolved to `any` for package consumers. Re-anchored.

Co-Authored-By: Claude <noreply@anthropic.com>
@philcunliffe

Copy link
Copy Markdown
Contributor Author

Verdict: approve with fixes (3 findings, all fixed and pushed as 3dd5133)

The substance is right. I re-derived the claims rather than taking the body's word
for them:

  • Finding 3 (silent successful restore) is genuinely fixed. replayPrevMalformed
    now returns the paths it wrote (src/core/config/client_detach_disk.js:326-334),
    both json branches surface them (:246, :565), and hyp detach prints one
    line each (src/core/commands/clients.js:603-605). Paths-only is real, not just
    asserted: nothing on the new path ever reads prevMalformed[dotted] into a message.
  • Finding 1 (legacy branch dropped backups) is genuinely fixed, and the extraction
    is a true share, so the wording cannot drift between branches. I checked the
    reachability claim independently: the current marker writer
    (hypaware-core/plugins-workspace/claude/src/settings.js:235-249) emits managed
    in the same object literal that ever emits prev_malformed/prev_base_url, so the
    branch is unreachable without a damaged marker, as claimed.
  • The classify-cwd widening is safe. LEGACY_CLAUDE_HOOK_PATTERN (:444)
    matches on the hyp claude-hook <sub> command, which no user writes, so broadening
    it cannot clobber a user's own hook.
  • The CI fix on 0e10288 is a correct diagnosis. stageHome() uses the mkdtemp
    prefix hyp-detach-disk-; a random suffix beginning x spells sk-x against the
    trailing disk-. Stripping settingsPath before the leak check is the right
    narrowing, and keeps the assertion pointed at the secret.
  • Test suite: 3086 pass / 8 fail with my fixes applied. The 8 are the pre-existing
    test/core/leave-command.test.js failures, identical by name to the baseline the
    body reports. npm run typecheck clean, npm run build:types clean.
  • @ref honesty: both new anchors resolve.
    LLP 0163#the-legacy-branch-replays-every-backup-the-marker-carries is the heading
    added at llp/0163-attach-backs-up-a-malformed-block.decision.md:259, and
    LLP 0044#conflict--back-up--override-restore-on-leave matches
    llp/0044-client-attach-on-join.decision.md:136. The paragraph that previously
    accepted the silent drop as corrupt-input behaviour is replaced rather than left
    contradicting the code, which is the thing that most often goes stale here.

Finding A - Medium: hyp detach --json grew a key the declared payload denies

src/core/commands/clients.js:591 writes payload.restored_paths, but
ClientResult (src/core/cli/types.d.ts:333-366), the declared shape of that
payload, was not extended. ClientResult is not internal: it is the return type of
the published integration helper (src/core/cli/integration.d.ts:29,
export declare function detach(...): Promise<ClientResult>) and is what
src/core/cli/integration.js:147 casts the parsed stdout JSON to. A typed consumer
therefore receives restored_paths on the wire while the contract says the field
does not exist. restoredValue has its counterpart declared at :359, so this is an
incomplete half of the pattern the PR is already following.

Fixed: added restored_paths?: string[] to ClientResult, with the
paths-never-values rule stated where a caller actually reads it.

Finding B - Low: the --json half of the new reporting had no test

test/core/client-detach-disk.test.js:1053 covers the prose line
(Restored env from the marker's malformed-block backup); nothing exercised the
json arm of writeCoreDetachOutput. Given the field exists to be reported, and the
--json payload is the surface most likely to be captured into a log, the leak check
matters at least as much there as in the prose.

Fixed: added #500 finding 3: hyp detach --json echoes restored_paths, and never the contents, asserting restored_paths deep-equals ['env'] and applying the same
secret check.

Finding C - Low (house style, documented rule): dangling type-import specifier

src/core/config/client_detach_disk.js:12 read
@import { DetachFromDiskResult } from './types.d.ts'. CLAUDE.md forbids exactly
this form by name, and this was the only remaining instance in src/ (299
@imports use the root-anchored src/... .js form, including line 11 of this same
file). It is not cosmetic: tsconfig.build.json does not copy .d.ts inputs, so
types/core/config/types.d.ts does not exist. I built the declarations and confirmed
the specifier resolved to nothing, meaning package consumers saw
DetachFromDiskResult as any. That is the very interface this PR extends with
restoredPaths, so the new field would have been invisible to them.

Fixed: re-anchored to '../../../src/core/config/types.js'; the emitted
types/core/config/client_detach_disk.d.ts now resolves.


Not actionable, recorded for the record

  • A damaged marker carrying no backups still reverses silently and partially.
    recordDamaged (src/core/config/client_detach_disk.js:500) keys on
    managed/prev_base_url/prev_malformed. Delete managed from a marker written
    over settings that had no prior ANTHROPIC_BASE_URL and no malformed block, and
    none of the three survive: ENABLE_TOOL_SEARCH and
    _CLAUDE_CODE_ASSUME_FIRST_PARTY_BASE_URL are then left behind with no warning. I
    checked whether a sharper test exists and it does not: the current marker is exactly
    the legacy shape plus those three fields
    (hypaware-core/plugins-workspace/claude/src/settings.js:235-249), so a marker in
    that state is shape-identical to a genuine pre-record one. Detecting it would mean
    guessing from env-key names, which is the guess this PR correctly refuses. Nothing
    is destroyed in that case, only left behind, so the destructive half of finding 1 is
    fully closed. Worth a sentence in LLP 0163 if it is ever revisited.
  • Finding 2's resurrection is unchanged, deliberately, and the PR says so in both
    the code and the doc. Agreed with the call: reversing it would discard a value
    rather than keep one, and it is now audible.
  • Findings 4a/4b left open and still standing as Open Questions in LLP 0163.
    Correct; neither is resolved by this change.

No changes to the PR body, no labels, no merge.

reverse() logged the failure half of the prev_malformed replay and not the
success half, so an org config drop could rewrite a block of the user's
settings file with nothing recorded anywhere. hyp detach prints a line per
restored path; this path has nobody at a terminal, so the log is the only
place it can be said. Paths, never values, as everywhere else.

Also refreshes two JSDoc paragraphs that still described the legacy hook
strip as session-context-only after this PR widened the pattern to
classify-cwd.

Co-Authored-By: Claude <noreply@anthropic.com>
@philcunliffe

Copy link
Copy Markdown
Contributor Author

Verdict: approve with fixes (1 finding + 1 nit, both fixed and pushed as 7944cfd)

Round 2, reviewed fresh at 3dd5133 rather than as a delta on round 1.

Round 1's three fixes: verified in the tree, not taken on trust

  • Fix 1 landed and is correct. restored_paths?: string[] is on ClientResult
    (src/core/cli/types.d.ts:369), sitting next to restored_value (:359), and
    src/core/commands/clients.js:591 is the writer it now declares.
  • Fix 2 landed. #500 finding 3: hyp detach --json echoes restored_paths, and never the contents (test/core/client-detach-disk.test.js:1083) drives the real
    runDetach(['claude','--json']), deep-equals restored_paths to ['env'], and
    applies the settingsPath-stripped sk-x check. It fails if the field is dropped.
  • Fix 3 landed, and I confirmed the effect rather than the edit.
    src/core/config/client_detach_disk.js:12 now reads
    '../../../src/core/config/types.js'. I re-ran npm run build:types and probed
    the emitted tree with tsc from outside the repo:
    types/core/config/client_detach_disk.d.ts:34 emits
    import type { DetachFromDiskResult } from '../../../src/core/config/types.js',
    and a deliberate bad property access against
    Awaited<ReturnType<typeof detachClientFromDisk>> errors with
    TS2339: Property 'definitelyNotAField' does not exist on type 'DetachFromDiskResult', while restoredPaths type-checks as
    string[] | undefined. Under any the first would have been silently accepted,
    which is the exact symptom round 1 diagnosed. The specifier also survives publish:
    package.json files ships src/, so <pkg>/src/core/config/types.d.ts is
    really there for the types/ tree to reach.

The rest of the PR, reviewed fresh

  • The replayPrevMalformed extraction is behaviour-preserving. Diffed against
    the inlined original: the only changes are the added restoredPaths accumulator
    and the inverted if. The record-driven branch's output is unchanged for every
    input that restored nothing.
  • No partial-write hazard in restoreAtDottedPath (:823). The return false
    for a non-object parent can only fire on a chain whose segments all pre-existed:
    the first absent segment creates a fresh {}, and everything below a fresh {}
    is absent too, so the walk can never create parents and then bail. The refusal
    path leaves no orphan objects behind.
  • The "slot is empty" test is sound. getAtDottedPath(...) !== undefined (:314)
    is an exact presence test here because JSON cannot encode undefined, so an
    explicit null on disk correctly blocks the restore rather than being overwritten.
  • The classify-cwd widening is safe and the "kept in step" claim is true.
    LEGACY_CLAUDE_HOOK_PATTERN (:444) is character-for-character the adapter's
    MANAGED_HOOK_PATTERN
    (hypaware-core/plugins-workspace/claude/src/settings.js:52) modulo a
    non-capturing group, and no third hyp claude-hook sub-command exists
    (.../claude/src/index.js:244,255).
  • recordDamaged's trigger set is complete. The current writer emits managed
    in the same object literal that can emit prev_base_url/prev_malformed
    (.../claude/src/settings.js:235-249), so no attach-written marker reaches the
    legacy branch, and any that does trips at least one of the three fields unless it
    carried none of them (see below).
  • prev_base_url presence really does mean "there was a prior".
    .../claude/src/settings.js:199 derives it from the live value only, so its
    absence on a damaged marker is not a lost backup.
  • Test suite: 3088 pass / 8 fail with my fix applied (3086/8 at 3dd5133; the
    +2 are mine). All 8 are test/core/leave-command.test.js, matching the
    origin/master baseline in test/core/leave-command.test.js: 8 of 11 tests fail on master (exit 1 from hyp leave), red since the tests were introduced #512 by name, and nothing else fails.
    npm run typecheck and npm run build:types clean. No em dash and no trailing
    semicolon in any added line.
  • @ref honesty: both anchors this PR added still resolve, and the one I added
    follows the same slug convention the corpus already uses
    (llp/0163-attach-backs-up-a-malformed-block.decision.md:225).

Finding A - Medium: the reconciler's undo still restores a block in total silence

src/core/config/action_attach.js:338 logs result.warning (the failure half of
the prev_malformed replay) as client_action.attach_reverse_warning, and there was
no counterpart for result.restoredPaths. So the exact silence #500 finding 3 exists
to close - a detach that rewrites a block of the user's settings.json and says
nothing - survived on the reverse() path, which is the worse of the two: hyp detach prints a line to a human who is standing there, whereas reverse() fires on
an org config drop with nobody at a terminal, so a log record is the only place the
rewrite can be recorded at all. The failure half being logged there while the success
half is not is the asymmetry that makes this a gap rather than a scope call, and
CLAUDE.md's Log-Driven Development rule asks for exactly this (emit structured logs
around lifecycle transitions; verify both external behaviour and emitted telemetry).

Fixed: client_action.attach_reverse_restored at
src/core/config/action_attach.js:361, with the same
component/operation/client/status attributes as its sibling, paths joined
into detail and never values. Two tests in test/core/action-attach.test.js:635
and :671: one asserts both dotted paths are named, one asserts the record is absent
when nothing was restored (the common case, which must stay quiet). LLP 0163
Consequences updated in the same commit, and its "both consumers render it" line
corrected to "both output modes", since the reconciler is a third consumer and was
not one of the two.

Finding B - Low (doc honesty, on the construct this PR changed): stale JSDoc after the widening

Two paragraphs still described the legacy hook strip as session-context-only after
the PR widened the pattern to also match classify-cwd:
src/core/config/client_detach_disk.js:461 ("strip the session-context hooks by the
claude-hook session-context command pattern") and the stripLegacyClaudeHooks doc
at :571. The scope of a deletion helper is not a detail to leave misdescribed in
the same commit that broadens it.

Fixed: both now point at {@link LEGACY_CLAUDE_HOOK_PATTERN} so they cannot
drift again, and the second says why the pattern covers more than the branch's
namesake (a damaged-record marker lands here with classify-cwd entries on disk).


Round 1's recorded non-actionable, re-examined independently: I agree, and it is not a blocker

The state in question: a marker carrying none of
managed/prev_base_url/prev_malformed, which reverses by the pre-record
convention with no warning.

I did not re-derive this from the comments, I executed it. Attach onto three
fixtures, delete _hypaware.managed, detach, diff before against after:

before after
{"env":{"MY_OWN":"keep-me"},"permissions":{"allow":["x"]}} {"env":{"MY_OWN":"keep-me","ENABLE_TOOL_SEARCH":"true","_CLAUDE_CODE_ASSUME_FIRST_PARTY_BASE_URL":"1"},"permissions":{"allow":["x"]}}
{} {"env":{"ENABLE_TOOL_SEARCH":"true","_CLAUDE_CODE_ASSUME_FIRST_PARTY_BASE_URL":"1"}}
{"hooks":{"SessionStart":[{"hooks":[{"type":"command","command":"my-own-thing"}]}]}} identical, plus the same two env keys

Every user-owned key survives byte-identical: MY_OWN, permissions, and the user's
own SessionStart hook. The marker, the gateway base URL and the managed hooks are
all correctly gone. The entire residue is the two keys hypaware itself added, and
the decisive fact is manageEnvAdditions
(hypaware-core/plugins-workspace/claude/src/settings.js:292):
if (!weOwnIt && Object.hasOwn(env, key)) continue - presence, not type - so attach
never writes over a value at those keys, and there is therefore no prior user value
for the partial reversal to fail to give back. Reaching this state also requires no
prev_base_url, which (per settings.js:199) means there was no prior base URL
either. So the partial reversal cannot lose a setting: there is provably nothing at
any of the affected keys that was ever the user's.

That is a stronger statement than round 1's "nothing is destroyed, only left behind",
and it is what makes this a leftover rather than a data-loss bug. I also agree it is
undetectable without guessing env-key names, which is the guess this PR is right to
refuse. Not a blocker. The leftovers are inert (ENABLE_TOOL_SEARCH=true and
_CLAUDE_CODE_ASSUME_FIRST_PARTY_BASE_URL=1 against a now-absent base URL, i.e. a
genuinely first-party endpoint) and sit in the file the user just hand-edited.

Also recorded, deliberately not raised as blockers

  • An env backup is structurally unrecoverable down the damaged-marker branch.
    The managed additions the branch may not delete keep env non-empty, so the replay
    always hits "in use again" and the backup dies with the marker
    (test/core/client-detach-disk.test.js:981 pins exactly this). Real destruction,
    but strictly better than the pre-PR behaviour, which dropped the same backup with
    no notice at all, and the honest alternatives (keep the marker; write the backup to
    a side file) are design changes well outside Follow-up: deferred review findings from PR #495 #500. LLP 0163 says it plainly.
  • A damaged marker whose port is also corrupted leaves the dead gateway URL in
    place and destroys prev_base_url with the marker. Corruption of corruption, and
    the "overridden externally" notice fires, so it is audible.
  • Finding 2's resurrection and open questions 4a/4b are unchanged and correctly
    left as Open Questions in LLP 0163.

No changes to the PR body, no labels, no merge.

@philcunliffe

Copy link
Copy Markdown
Contributor Author

TRIAGE rung (LLP 0017): the review fix-loop exhausted its 2 rounds at head
7944cfd. Re-derived every claim from the tree rather than the review
records' word:

Every residual is non-blocking (leftover inert env keys from a hand-damaged
marker, a deliberately-unchanged resurrection behavior, a structurally
unrecoverable malformed-block backup in an already-corrupted-marker edge
case, and open design questions unrelated to #500) — filed as
#531 for the record, with a backlink to this PR. This PR is safe to ship.

@philcunliffe
philcunliffe marked this pull request as ready for review July 31, 2026 05:45
@philcunliffe philcunliffe added the neutral:approved neutral reviewed this and holds it for a maintainer merge (own or adopted PR; LLP 0025/0030) label Jul 31, 2026
@philcunliffe
philcunliffe merged commit c4df3e2 into master Jul 31, 2026
9 checks passed
@philcunliffe
philcunliffe deleted the fix/issue-500 branch July 31, 2026 17:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

neutral:approved neutral 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.

Follow-up: deferred review findings from PR #495

1 participant