Skip to content

feat: delete CoDev-authored configs on restore, gated on authorship#202

Merged
quickbeard merged 2 commits into
mainfrom
feat/delete-codev-authored-configs-on-restore
Jul 17, 2026
Merged

feat: delete CoDev-authored configs on restore, gated on authorship#202
quickbeard merged 2 commits into
mainfrom
feat/delete-codev-authored-configs-on-restore

Conversation

@quickbeard

Copy link
Copy Markdown
Owner

Why

restoreKind's no-backup branch left every live config in place (kept-live). So a fresh install — where CoDev wrote the config and there was nothing to back up — left its own config behind after codevhub remove, still pointing at a now-deleted ~/.codev-hub, with the user told to clean it up by hand.

This deletes those files, but only when we can actually show they're ours.

Relationship to #196

#196 moved this in the opposite direction (delete → keep), on the grounds that "deleting a config we can't prove predated CoDev is destructive." That objection was correct, and this PR doesn't revert it.

The code #196 removed deleted unconditionally, on the stated theory that "a file present without a backup must be CoDev-authored." That theory is false, because restore consumes the backup (renameSync). "No backup + live file" equally describes:

  1. CoDev wrote the config and nothing preceded it — ours to delete.
  2. A second restore, where the live file is the pristine original the first run just reinstated.
  3. A hand-written config for a tool CoDev never configured — and both remove and the bare restore sweep visit every tool.

Cases 2 and 3 are real data-loss paths, and they're why #196 was right to stop the bleeding. This PR proves authorship instead of assuming it, so case 1 is deleted and 2/3 are still kept.

How

The gate is isCodevAuthored(kind), which dispatches to the per-kind marker detectors codevhub model already uses — no new parsing logic. Each returns false for a missing or unparseable file, so anything unattributable stays kept-live.

The two Claude auth files have no marker key of their own:

  • ~/.claude.json — matched by whole-file shape: exactly the {hasCompletedOnboarding: true} stub resetClaudeAuth writes. A file carrying real projects/history/mcpServers survives.
  • ~/.claude/.credentials.json — always ours, since CoDev never writes this file, only removes it; a live one can only postdate CoDev. Accepted trade-off: someone who never configured Claude via CoDev gets logged out. Recoverable by re-authenticating, no data lost.

No cross-kind inference (e.g. reading settings.json to decide the credentials' fate) — settings.json restores first and erases its own marker, which would make results order-dependent.

Notes for review

Two things worth a look beyond the core diff:

The tests would have lied. Every test named "keeps the live CoDev config when no backup exists" seeded fake content ('{"marker":"codev-live"}') that no real detector recognizes. Under marker-gating they all still pass — while silently exercising the user-authored branch their names claim not to cover. Each is split into a CoDev case and a user case, with CoDev fixtures built by calling the real writers (configureClaudeCode, configureCodex, …) so the gate and the writers can't drift apart. New coverage for restore-twice and never-configured-tool.

One hint had to be reworded, not tweaked. RemoveApp's "Delete manually for a clean state" becomes actively wrong advice: a file is now kept precisely because it's the user's own.

Also corrects two stale claims in the same AGENTS.md section (runRestore's exit code, and the missing continue alias).

Verification

  • 922 tests, typecheck, lint, and bundle smoke-test all pass.
  • A green suite alone doesn't prove the delete branch, so the built dist/index.js was also driven against a scratch HOME:
    1. Real CoDev config, no backup → deleted.
    2. Hand-written config for an unconfigured tool → kept, sweep still exits 1.
    3. Restore twice → the reinstated original survives the second run.

🤖 Generated with Claude Code

quickbeard and others added 2 commits July 17, 2026 13:32
`restoreKind`'s no-backup branch left every live config in place (`kept-live`),
so a fresh install — where CoDev wrote the config and there was nothing to back
up — left its own config behind after `codevhub remove`, pointing at a deleted
~/.codev-hub. This deletes those files, but only when we can show they're ours.

#196 moved this the other way (delete -> keep) because "deleting a config we
can't prove predated CoDev is destructive". That objection was right: the code
it removed deleted unconditionally, on the theory that a file with no backup
must be CoDev-authored. That theory is false, because restore *consumes* the
backup (renameSync) — "no backup + live file" also describes a second restore
whose live file is the pristine original the first run just reinstated, and a
hand-written config for a tool CoDev never configured (both `remove` and the
bare `restore` sweep visit every tool). Rather than revert #196, this proves
authorship instead of assuming it.

- `configure.ts`: add `deleted` to RestoreStatus and gate the no-backup branch
  on a new `isCodevAuthored(kind)`, which dispatches to the per-kind marker
  detectors `codevhub model` already uses. Each returns false for a missing or
  unparseable file, so anything unattributable stays `kept-live`.
- The two Claude auth files have no marker key: `claude-json` is matched by
  whole-file shape (`isCodevClaudeJsonStub` — exactly the
  `{hasCompletedOnboarding: true}` stub resetClaudeAuth writes, so a file with
  real projects/history survives), and `claude-credentials` is always ours since
  CoDev never writes it, only removes it. No cross-kind inference: settings.json
  restores first and would erase its own marker, making results order-dependent.
- `restore.ts`: report deletes, count them as action in the sweep (a delete-only
  sweep would otherwise exit 1 with "No backups found"), and add a `never`
  default — that switch returns void, so a missing case printed nothing instead
  of failing to compile.
- `remove.ts` / `RemoveApp`: report deletes in the aggregated step, and reword
  the kept-files hint. "Delete manually for a clean state" is now wrong advice:
  a file is kept precisely because it's the user's, not CoDev's.
- Tests: every "keeps the live CoDev config" test seeded fake marker content no
  detector recognizes, so all of them would still pass while silently testing
  the user-authored branch. Split each into a CoDev case and a user case, with
  CoDev fixtures built by the real writers so the gate and writers can't drift.
  Adds coverage for restore-twice and never-configured-tool.
- AGENTS.md: document the four states and the gate; correct two stale claims in
  the same section (runRestore's exit code, the missing `continue` alias).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`codevhub restore [agent] --force` and `codevhub remove --force` delete a
backup-less live config whoever wrote it, skipping the authorship check that
normally preserves the user's own files. Deliberately absent from help.ts, the
README, and all UI copy — an escape hatch discoverable only from source, and
nothing invokes it on the user's behalf.

Scope is the gate only. A `*.backup` still wins and is still restored: that file
is the user's pre-CoDev original and reinstating it is the point of the command,
so force has no business overriding it.

- `force = false` threads through restoreKind / restoreTool / runRestore /
  runRestoreAll / runRemove / RemoveApp. Long form only — no `-f` alias, so the
  reflex `-f` from `upload`/`login` can't unconditionally delete configs.
- `restore` now finds its agent arg with `.find(a => !a.startsWith("-"))`;
  `args[0]` would have read `--force` as the agent name and errored on the sweep.
- Reporting stays honest: restoreKind evaluates isCodevAuthored even when
  forcing and sets `RestoreResult.forced = !authored`. The default delete message
  asserts "CoDev wrote it", which is a false claim about a user's own file, so
  forced deletes get their own wording. remove counts them separately
  (`deleted 2 files (no backup, 1 forced)`) and the NDJSON log carries `forced`.
- Fixes a latent bug in restoreTool: `CLAUDE_RESTORE_KINDS.map(restoreKind)`
  passes the array index into `force`, so .claude.json and .credentials.json
  (indices 1 and 2) would be force-deleted on every run. Now
  `.map((kind) => restoreKind(kind, force))`, pinned by a regression test that
  was confirmed to fail against the old form.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@quickbeard
quickbeard merged commit 0e17770 into main Jul 17, 2026
4 checks passed
@quickbeard
quickbeard deleted the feat/delete-codev-authored-configs-on-restore branch July 17, 2026 07:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant