Skip to content

fix: land remaining v0.6 blockers (codex passes 3-6, /code-review, #183 security, #184 docs)#185

Merged
bminier merged 7 commits into
devfrom
fix/v0.6-remaining-blockers
May 25, 2026
Merged

fix: land remaining v0.6 blockers (codex passes 3-6, /code-review, #183 security, #184 docs)#185
bminier merged 7 commits into
devfrom
fix/v0.6-remaining-blockers

Conversation

@bminier

@bminier bminier commented May 25, 2026

Copy link
Copy Markdown
Owner

Summary

PR #178 landed only its first 5 cluster commits on `dev`. PR #184 then merged into `fix/v0.6-release-blockers` (the feature branch), not into `dev`. Net result: 7 v0.6 release-blocker commits never reached `dev` — including the audit-log path-injection fix (#183) and several codex-pass correctness fixes.

This PR cherry-picks those 7 commits onto a fresh branch off `dev`:

Commit What it fixes
`884f1c8` → `884f1c8` Cover post-confirm degraded log; park unknown archives after valid (codex pass 3)
`6da4de0` → `d68a89d` Absolutize CLI project paths; coalesce restore writes per file (codex pass 4)
`e4b2734` → `1c32d1b` Absolutize `--home-dir` always; recheck audit log even under `--yes` (codex pass 5)
`920219e` → `6acec8f` Route Move-to project-submenu picks to the selected project (codex pass 6, #111)
`973ce76` → `00b0999` Reload current project after cross-project Move-to; doc the config-dir env hook (/code-review)
`e901b51` → `81012ba` Audit-log path allowlist (#183 security) + CLI project_dir (#180)
`f31dd55` → `6fd3073` Post-v0.6 docs sync — audit-log security invariants, refusal UX, threat model

Cherry-picks were clean — no manual conflict resolution. Each commit's original message and authorship preserved.

Why this is its own PR

GitHub merged PR #184 into `fix/v0.6-release-blockers`, not into `dev`, because that was its base. The `Closes #N` keywords on `e901b51` / `81012ba` therefore never fired, leaving #183 + #180 open. This PR's merge to `dev` will close them.

Test plan

v0.6 release-gate impact

Once this lands, the v0.6 release-gate (#143) clears its final code-side checkbox. Then Actions → Release Please → Run workflow to cut the tag.

Closes #183, closes #180, closes #177.

🤖 Generated with Claude Code

bminier and others added 7 commits May 25, 2026 04:24
…ter valid

Codex 3rd-pass: two more fixes before the release gate clears.

[P1] CLI run_cli_restore's post-confirm re-read ignored the
read_all `skipped` count. A malformed line appended during the
confirm prompt would slip past the tail-id check (corrupt entries
don't shift the trailing ULID — they're invisible to it) and apply
the stale restore against a partial log. Same shape as the GUI
gate broadened in the previous commit; the CLI gate now mirrors it.

[P2] `archive_sort_key` returned `(stem, u32::MAX)` for unknown
archive shapes, but a tuple sorts on the first element first — so a
stray `audit-0000.jsonl` lex-sorted *before* `audit-2026-05.jsonl`
regardless of the MAX. Restructured to `(is_unrecognized, year_month,
suffix)`: the bool sorts parseable archives ahead of every unknown
shape, with year_month + suffix providing chronological order within
each group.

Tests:

  - `cli_undo_refuses_when_audit_log_degrades_during_confirm_prompt`:
    spawn `undo`, wait for prompt, inject a malformed line, send y,
    assert non-zero exit + "degraded" / "unreadable" on stderr.
  - `archive_sort_key_parks_unknown_shapes_after_valid` covers both
    the direct key comparison and the actual sort.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… file

Codex 4th-pass surfaced two real release-blockers exposed by the
multi-key restore-plan dedupe fix:

[P1] CLI `--project-dir` was stored verbatim in `ScopePaths`. A
relative path landed in the audit log's `file_path` values, so a
later `undo` / `redo` / `restore` run from a different cwd would
resolve those paths against the new cwd — restoring or creating
files under the wrong directory entirely. `resolve_paths` now
absolutizes both `--project-dir` and `--home-dir` against the
process cwd before deriving any scope paths.

[P1] `apply_restore_plan` issued one save per `plan.targets[i]`. A
restore-to-point window touching two top-level keys in the same
settings.json (now possible thanks to the dedupe fix in #163 /
#167) loaded the file twice with the same stamp, mutated each
load for its own key, then saved twice. The second save would
either fail with `ConcurrentModification` (the first save changed
the file out from under it) or, on coarse-mtime filesystems,
overwrite the first key's restoration with a doc that didn't
include that change. Restructured to load once per unique file
and accumulate every plan target's mutation into one `new_doc` +
one save; phase 3 still returns one `audit::Side` per target so
the wire format is unchanged.

Tests:

  - `apply_restore_plan_coalesces_two_targets_in_one_file`:
    writes a file with two keys at their post-change state, builds
    a plan with two targets on that file, applies, asserts both
    keys are restored in a single save.
  - `resolve_paths_absolutizes_relative_project_dir`: pins the new
    behavior so a future refactor can't quietly drop it.
  - Existing `resolve_paths_*` tests updated to use tempdir paths
    (the literal `/tmp/...` strings broke on Windows where they're
    not absolute, and absolutize() would resolve them against the
    test cwd).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…--yes

Codex 5th-pass: two more narrow but real edge cases.

[P1] `resolve_paths`'s early-return branch (no `--project-dir`)
passed the raw `--home-dir` straight into `scope::resolve_with_home`
without absolutizing it. A relative `--home-dir` would persist
relative `file_path` values for user / user-local scopes into the
audit log, with the same wrong-cwd-on-restore failure mode the
project-dir fix already addressed. Absolutizing now happens at the
top of `resolve_paths` so both branches benefit.

[P1] `run_cli_restore` skipped the post-confirm log re-read entirely
under `--yes`. Even without a prompt window, there's a real (though
narrower) window between the caller's initial `read_audit_log` and
the apply call where a concurrent GUI/CLI append can land. Lifted
the re-read out of the `!yes` branch so it runs immediately before
`apply_restore_plan` regardless of prompt path. The tail-id +
degraded-log refusals now cover every restore apply.

Tests: existing coverage (the prompt-window staleness test and the
degraded-log test) still exercises the relevant paths. The new
no-prompt window is short enough that a deterministic test would
require subprocess timing as fragile as the prompt-based ones we
already have, and the read_audit_log gate at the cmd_* level
already catches the broader case.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…111)

Codex 6th-pass [P1]. The cross-project Move-to submenu (#111) let the
user pick `OtherProject > Local` / `OtherProject > Project` for a
rule, but the click handler dropped `project.root` and called
`onPick(target)` with only the scope. `main.ts`'s `moveLeaf` then
invoked `apply_move_leaf` with the *current* `state.projectDir`, so
the rule landed in the currently-open project's settings file — not
the project the user clicked. A user with two known projects could
silently modify the wrong repository's Claude settings.

Fix plumbs `project.root` through three layers:

  - `buildMoveToSubmenu`'s `onPick` callback now takes
    `(target, projectRoot?)`. Cross-project items pass `project.root`;
    same-project items keep the old single-arg shape.
  - The two `buildMoveToSubmenu` callers (`leafContextMenuItems`,
    `combinedChipContextMenuItems`) forward the optional projectRoot
    into `props.onMoveLeaf` via a new `MoveOptions.projectDir`.
  - `moveLeaf` in `main.ts` uses `opts?.projectDir ?? state.projectDir`
    so the override wins when set.

Notes:

  - For cross-project moves from User scope (the intended use), this
    works fully: the resolver finds the source under `~/.claude/` and
    the destination under the override's project root.
  - For cross-project moves whose source is the current project's
    `Local`/`Project`, the backend's single-project_dir command can't
    express "read from project A, write to project B" — those will
    now error with "source not found in project B" instead of
    silently corrupting project A's settings, which is the meaningful
    half-fix. Full cross-project moves with non-user sources need a
    backend `project_dir_from` / `project_dir_to` split; that's a
    follow-up issue, not a v0.6 blocker.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…config-dir env hook

Claude /code-review against the codex-cleared PR found two more
real issues:

[P1] `moveLeaf` called `await load(projectDir)` after the apply,
using the cross-project override path when set. For a User-scope
rule moved into OtherProject, the GUI would silently context-switch
to OtherProject — scopes panel re-renders for the other project,
file watcher reinstalls on the other paths, recent-projects LRU
promotes the other project. The user expected to keep viewing the
current project (the rule still lived on disk wherever the source
scope did). Now `load(state.projectDir)` reloads the originally
viewed project; the destination write still landed correctly.

[doc] `CLAUDE_SCOPE_CONFIG_DIR` env hook was added for tests but
the doc comment described it as "also a useful escape hatch for
users." That's misleading — the override has no UI affordance, no
sandbox banner, so a user who sets it can silently desync their
real config from the one ClaudeScope reads. Rewrote the comment to
clearly tag it as a developer/test hook and point at the right
fix (route through RuntimeOverrides + sandbox banner) if we ever
want to expose it for real.

Tests: existing 268 lib + 20 CLI + 120 JS pass; no behavior the
existing tests covered changed (the cross-project Move-to flow is
exercised manually, no integration test for the cross-project
case exists yet — that lives with the #179 backend follow-up).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…set (#183, #180)

Closes #183 (security — path injection via audit log). Surfaced by
`/security-review` on PR #178 at confidence 0.95. Bundles the #180
follow-up because the security gate exposed it as an immediate
blocker.

## The vulnerability (#183)

The undo/redo/restore-to-point pipeline trusted the `file_path:
PathBuf` field of `audit::Side` records deserialized from
`~/.claude/claude-scope/audit.jsonl` and wrote to that path directly
via `io_atomic::save`. An attacker who could append a single
well-formed JSON line to `audit.jsonl` (cloud-sync collision, a
previously-compromised tool with user-scope write, a malicious
package postinstall, etc.) could cause ClaudeScope to write
attacker-controlled JSON to any path the user could write to. The
parent-directory auto-creation in `atomic_write_json` (`fs::
create_dir_all`) extended the primitive to "create any missing dirs
along the way." `claude-scope-cli undo --yes` bypassed the
confirmation preview entirely, making the attack scriptable.

The codex+code-review iteration loop on PR #178 had focused on
audit-log integrity (rotation, locking, dedupe, atomicity). The path
allowlist gap survived all six codex passes and the Claude
code-review pass — it's the kind of issue the iterative review
caught only when explicitly framed as a security review.

## Fix

New `validate_audit_records(records, home_dir)` helper enforces a
per-record allowlist: each Side's `file_path` must equal one of the
four resolved `ScopePaths` slots (`local`, `project`, `user_local`,
`user`) for the record's own `project_dir`, against the active home
override. Canonicalization handles `/var → /private/var` (macOS)
and `\?\` extended-path (Windows) drift. Basename allowlist
(`settings.json` / `settings.local.json`) is defense in depth.

Validation runs at every audit-log boundary:

  - GUI: `undo_redo_target` validates the resolved target record
    before returning. `restore_to_point_preview` and
    `apply_restore_to_point` validate the whole window
    `&records[target_idx..]` — each record carries its own
    `project_dir`, so the allowlist scopes per-record.
  - CLI: `cmd_undo` / `cmd_redo` / `cmd_restore` validate at the
    same boundary the audit log is read at. `--yes` no longer
    bypasses validation.

`build_restore_plan` and `plan_restore_to` stay pure (no signature
change) so the 17 existing test sites that call them with manually-
constructed plans don't need updating.

## #180 fix (bundled)

CLI `cmd_move` previously recorded `project_dir: None` on every
audit record. The new validator correctly refused those records as
"user-only scope only" — which broke `undo` / `redo` / `restore`
against any CLI-initiated move. Fixed in the same commit: the
record now carries `Some(paths.project_dir.clone())` to match the
GUI's `apply_move_leaf` shape. Tests updated.

## Tests

  - `validate_audit_records_refuses_path_outside_scope_allowlist`:
    hostile Side pointing at `tmp.path()/malicious-target.json`
    rejected with "path injection refused" message.
  - `validate_audit_records_refuses_non_settings_basename`: even
    a path under the legitimate project directory but with the wrong
    basename is rejected.
  - `validate_audit_records_accepts_legitimate_record`: sanity that
    the gate doesn't fail-closed on real records.
  - `cli_undo_refuses_audit_record_pointing_outside_scope_allowlist`
    (integration test): writes a hostile audit record via the real
    `audit::` API, runs `claude-scope-cli undo --yes`, asserts
    non-zero exit + path-injection-refused message + the attacker's
    target file does NOT exist (write was prevented before any I/O).
  - Existing CLI tests `logged_move` helper updated to thread
    project_dir through so the new validator accepts the seed
    records.

Total: 257 lib + 21 CLI integration + 120 JS tests pass.

Closes #183, closes #180.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…eat model

`/document-release` sweep over the v0.6 ship (PRs #178 + #184). The
audit-log architecture doc, CLI reference, undo/redo user guide,
and the security page all had drift relative to what shipped — the
six codex passes + Claude code-review pass + security review added
behaviors that the docs hadn't caught up to.

Updates:

  - `docs/src/architecture/audit-log.md`: new
    "Security invariants" section documenting the path allowlist
    (#183), degraded-log refusal (#170), and tail-ID stalecheck
    (#165/#171). Also bumped the stale `claude_scope_version`
    example from 0.3.0 to 0.6.0 in the record-shape snippet.
  - `docs/src/security.md`: new "Threat model" section covering
    hostile audit-log entries (linking to the allowlist invariants)
    and hand-edited settings files (linking to atomic-writes), plus
    explicit out-of-scope items (multi-user systems, resource
    exhaustion, binary tampering).
  - `docs/src/user-guide/undo-redo.md`: documented the three new
    user-visible refusals — the "log degraded" tooltip / button
    disable, the "log changed since the preview" staleness error,
    and the "path injection refused" hostile-record message.
  - `docs/src/reference/cli.md`: new "Refusals" subsection covering
    the same three error states for `undo` / `redo` / `restore`,
    including the explicit note that `--yes` does not bypass any of
    them.

README.md already had v0.6 feature coverage (audit log bullet +
keybindings table entry) from earlier commits; no further edits.

No source-code TODOs are stale — the audit/restore code is comment-
dense but every comment names a live invariant.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@bminier bminier merged commit b69d167 into dev May 25, 2026
14 checks passed
@bminier bminier deleted the fix/v0.6-remaining-blockers branch May 25, 2026 13:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant