feat(doctor): add --dry-run flag to preview config changes without applying#79734
feat(doctor): add --dry-run flag to preview config changes without applying#79734smonett wants to merge 6 commits into
Conversation
…plying Adds a --dry-run option to 'openclaw doctor --fix' that shows what changes would be applied to the config without writing anything. Outputs a structured diff of proposed changes (additions, removals, modifications) and exits. The pendingChanges/candidate object already contains the full proposed change set before finalizeDoctorConfigFlow decides whether to write. --dry-run serializes the diff between cfg and candidate as a flat key-path comparison and emits it via note(), then returns shouldWriteConfig: false. This matches the pattern established by 'sessions cleanup --dry-run'. Changes: - src/commands/doctor.types.ts: add dryRun field to DoctorOptions - src/commands/doctor/finalize-config-flow.ts: dry-run diff logic + flattenObject helper - src/commands/doctor-config-flow.ts: pass dryRun through the flow - src/cli/program/register.maintenance.ts: wire --dry-run CLI flag - src/flows/doctor-health.ts: skip config-write assertion in dry-run mode - docs/cli/doctor.md: document --dry-run option - src/commands/doctor/finalize-config-flow.test.ts: 3 test cases for dry-run behavior Closes #79166
|
Codex review: needs real behavior proof before merge. Reviewed June 2, 2026, 1:12 AM ET / 05:12 UTC. Summary PR surface: Source +65, Tests +60, Docs +3. Total +128 across 8 files. Reproducibility: yes. by source inspection: the PR head keeps repair mode enabled through prompter/health paths and serializes raw config values; no live command was run because this review is read-only. Review metrics: 1 noteworthy metric.
Merge readiness Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch. Rank-up moves:
Proof guidance:
Risk before merge
Maintainer options:
Next step before merge
Security Review findings
Review detailsBest possible solution: Land a command-wide doctor dry-run contract that clears repair mode before preflight/prompter/health contributions, reports skipped legacy repair slots, redacts config values, and includes no-write/no-state proof. Do we have a high-confidence way to reproduce the issue? Yes by source inspection: the PR head keeps repair mode enabled through prompter/health paths and serializes raw config values; no live command was run because this review is read-only. Is this the best way to solve the issue? No. The feature direction is useful, but the best fix is a command-wide read-only preview built on the structured repair/diff contract rather than a finalization-only config diff with repair mode still active. Full review comments:
Overall correctness: patch is incorrect AGENTS.md: found and applied where relevant. Codex review notes: model gpt-5.5, reasoning high; reviewed against ebf20241bd17. Label changesLabel justifications:
Evidence reviewedPR surface: Source +65, Tests +60, Docs +3. Total +128 across 8 files. View PR surface stats
Security concerns:
Acceptance criteria:
What I checked:
Likely related people:
What the crustacean ranks mean
Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics. How this review workflow works
|
clawsweeper flagged that --fix --dry-run could still reach mutating repair paths in two ways: 1. runDoctorRepairSequence has real side effects (plugin installs via installPluginFromNpmSpec/ClawHub, index writes via writePersistedInstalledPluginIndexInstallRecords). It was called whenever shouldRepair=true, which includes the --fix --dry-run case. Fix: guard with `if (shouldRepair && !dryRun)`. 2. runWriteConfigHealth checks ctx.configResult.shouldWriteConfig || ctx.cfg !== ctx.cfgForPersistence. If any health contribution mutated ctx.cfg after the config flow returned shouldWriteConfig:false, the config write would fire anyway -- even in dry-run mode. Fix: add an authoritative early-return guard at the top of runWriteConfigHealth so dry-run can never write regardless of later cfg mutations. Together these make --dry-run a genuine read-only preview: config-level mutations collected before the repair sequence are still shown in the diff output; the repair sequence itself and all config writes are completely skipped.
|
Addressed the security finding from clawsweeper's review (commit eabb075). Two gaps closed: Gap 1 —
|
…exclusions Expands the --dry-run option entry from a one-liner to a precise behavior spec: what is skipped (repair sequence, plugin installs, index state writes, config persistence), what diff is produced (pre-repair mutations: normalization, legacy migration, auto-enable), precedence over --fix/--repair, and Nix-mode compatibility. Adds a Notes entry documenting the two-layer write guard: the config flow returning shouldWriteConfig: false plus an authoritative early-return backstop in the write step that prevents late health contribution mutations from triggering a write in dry-run mode. Adds openclaw doctor --fix --dry-run to the Examples block.
|
Follow-up commit 69f8e43 updates
PR is ready for maintainer review. |
Captures lessons from PR #79734 dry-run gap review: write-path audit, side-effect audit for preview flags, authoritative guard placement, test coverage gate, clawsweeper acceptance criteria, docs-in-same-commit rule, and security-sensitive flags matrix.
|
CI note: the three failing checks ( |
docs/contributing/ is not an existing upstream directory; adding an internal contributor checklist to upstream scope is out of scope for this PR. Moving checklist to the submitter's workspace only.
|
This pull request has been automatically marked as stale due to inactivity. |
Summary
Adds
--dry-runtoopenclaw doctor --fixso operators can preview proposed config changes before committing to them. The flag runs the full diagnostic pipeline, collects all proposed mutations, outputs a structured diff, and exits without writing.Closes #79166.
Motivation
openclaw doctor --fixis the most common maintenance command, but it applies changes without preview. There is no way to see what--fixwould do before it does it.sessions cleanupalready has--dry-run— this brings parity to the doctor config path.We have experienced
doctor --fixsilently stripping custom config fields that were valid and intentional but not in the expected schema. A--dry-runflag would have prevented each of those incidents.Design
The
pendingChangesobject andcandidateconfig already contain the full proposed change set beforefinalizeDoctorConfigFlowdecides whether to write.--dry-runleverages this existing plumbing:dryRunis true, the config preflight runs withrepairPrefixedConfig: trueso legacy migration and normalization steps populatecandidate.finalizeDoctorConfigFlowcomparescfg(current) againstcandidate(proposed) using a flat key-path diff.note()as+(added),-(removed),~(modified) lines.shouldWriteConfig: false— no config mutation occurs.--dry-runtakes precedence over--repair/--fixif both are specified.Security fixes (post-submission, commit eabb075)
After clawsweeper's review identified two write-path gaps, both were fixed:
Gap 1 —
runDoctorRepairSequenceside effects in dry-run mode.The repair sequence has real filesystem side effects:
installPluginFromNpmSpec/installPluginFromClawHub(package installs) andwritePersistedInstalledPluginIndexInstallRecords(index state writes). It was called whenevershouldRepair === true, which includes--fix --dry-run. Fix:if (shouldRepair && !dryRun)— repair sequence skipped entirely in dry-run. Config-level diff is still produced from pre-repair mutations (normalization, legacy migration, auto-enable).Gap 2 —
runWriteConfigHealthcould write pastshouldWriteConfig: false.runWriteConfigHealthwrites whenctx.configResult.shouldWriteConfig || ctx.cfg !== ctx.cfgForPersistence. If any health contribution mutatedctx.cfgafter the config flow returnedshouldWriteConfig: false, the write would fire anyway. Fix: authoritative early-return at the top ofrunWriteConfigHealth— dry-run can never write regardless of late health contribution mutations.Changes
src/commands/doctor.types.tsdryRun?: booleantoDoctorOptionssrc/commands/doctor/finalize-config-flow.tsflattenObjecthelper (~40 lines)src/commands/doctor-config-flow.tsdryRunthrough the flow; enable preflight in dry-run; guard repair sequence with!dryRunsrc/cli/program/register.maintenance.ts--dry-runCLI flagsrc/flows/doctor-health.tsassertConfigWriteAllowedInCurrentModein dry-run modesrc/flows/doctor-health-contributions.tsrunWriteConfigHealthentrydocs/cli/doctor.md--dry-runoption, safety contract, and Nix-mode compatibilitysrc/commands/doctor/finalize-config-flow.test.tsReal behavior proof
openclaw doctor --fixapplies config changes without preview. No--dry-runor--previewflag exists. This adds--dry-runto show proposed changes without writing. Filed as [Feature] Doctor dry-run / diff mode #79166.tsc --strict --noEmiton the new code paths, zero errors). Ranopenclaw doctor --non-interactiveon the live gateway to confirm current doctor flow is unaffected. Traced the full flag wiring path: CLI registration (register.maintenance.ts) →DoctorOptionstype →loadAndMaybeMigrateDoctorConfig→finalizeDoctorConfigFlowdry-run branch.TypeScript type verification (standalone, our new code extracted):
The
dryRunflag path infinalizeDoctorConfigFlowproduces a flat key-path diff ofcfgvscandidateand returnsshouldWriteConfig: false. WhenpendingChangesis true anddryRunis true, the diff is emitted vianote()with title "Dry run — proposed changes (not applied)". When no changes exist, "No config changes detected." is emitted.--non-interactive). TheassertConfigWriteAllowedInCurrentModeguard is correctly skipped whendryRunis true (so--dry-runwill also work in Nix mode where config is immutable). Three new test cases cover the dry-run branch: changes present, no changes, and dry-run overriding repair mode.pnpm build && pnpm check && pnpm test— the@openclaw/fs-safegit-hosted dependency fails its prepack script in a fresh clone on macOS arm64. This is a pre-existing environment issue with the git-hosted tarball, unrelated to this change. CI handles the full suite.CI note
Three CI checks (
build-artifacts,build-smoke,check-additional) are failing due to a pre-existing upstream i18n drift inui/src/ui/chat/grouped-render.ts— a hardcoded"Tool output"string not in locale files. None of our changed files touch the UI or i18n surface. All 81 checks covering our actual changes passed.Tests
3 new test cases in
finalize-config-flow.test.ts:shouldWriteConfig: falseChecklist
docs/cli/doctor.md) — option entry, safety contract note, Nix-mode compatibility, example added