Skip to content

Critical: docs find-replace --dry-run ignored — all four variants mutate, empty-replace causes data loss #542

@chrismdp

Description

@chrismdp

Summary

The --dry-run flag is silently ignored by gog docs find-replace. All four invocation variants execute the mutation against the live document. This is a data-loss-class bug — the empty-replacement variant has corrupted production documents in the past.

Severity

Data-loss class. The empty-replacement + --first + --dry-run combination can leave documents with matched text deleted but not replaced (because the underlying implementation is a non-atomic delete-then-insert pair). I lost content from a signed Letter of Engagement to this exact pattern on 2026-03-25.

Steps to Reproduce

Tested on v0.11.0-33-gd3ef84d (current feat/docs-replace-cmd HEAD).

DOC=<a fresh doc with the strings used below>

# Variant 1 — positional replacement + --dry-run
gog docs find-replace $DOC "old text" "new text" --dry-run --first
gog docs cat $DOC | grep "new text"   # MUTATED

# Variant 2 — --content-file + --dry-run
echo "PAYLOAD" > /tmp/p.txt
gog docs find-replace $DOC "anchor" --content-file /tmp/p.txt --dry-run --first
gog docs cat $DOC | grep "PAYLOAD"    # MUTATED

# Variant 3 — --content-file + --format markdown + --dry-run
echo "**bold**" > /tmp/p.md
gog docs find-replace $DOC "anchor" --content-file /tmp/p.md --format markdown --dry-run --first
gog docs cat $DOC | grep "bold"       # MUTATED

# Variant 4 — empty replacement + --dry-run + --first  (DATA LOSS)
gog docs find-replace $DOC "anchor" "" --dry-run --first
# Output reports: "Google API error (400 badRequest): Insert text requests must specify text to insert"
gog docs cat $DOC | grep "anchor"     # GONE — anchor text was deleted before the insert failed

Output of Variant 1 (positional + --dry-run)

documentId  <docId>
find        old text
replacements        1

No "would replace" preview language, no dry-run indicator. The replacements: 1 line is the same line the non-dry-run path emits when it actually mutates.

Expected Behavior

  • --dry-run should print what would happen and exit 0 without making any API mutation calls.
  • For --first flows specifically, the implementation should be transactional or at least guard against partial-application data loss when a downstream insert request fails.

Actual Behavior

  • --dry-run is silently ignored. The mutation happens.
  • For --first with empty replace, the delete-then-insert pair is non-atomic: the delete succeeds, the insert fails (Insert text requests must specify text to insert), and the matched text is lost with no recovery path.

Environment

  • gog --version: v0.11.0-33-gd3ef84d (d3ef84deb6ca 2026-05-01T13:38:07Z)
  • Branch: feat/docs-replace-cmd
  • OS: Linux x86_64
  • Auth: stored OAuth via system keyring

Suggested Fix

  1. Honour --dry-run end-to-end. In find-replace, gate every code path that calls a Google Docs mutation API on !cfg.DryRun. Print the planned operations (occurrences found, requests that would be sent) and return.
  2. Make the --first path atomic. Bundle the deleteContentRange and insertText requests into a single documents.batchUpdate call so they apply atomically, or guard against the empty-insertText case before the delete fires. Currently the user can lose data by passing an empty replacement.
  3. Validate replace content is non-empty before any API call when the implementation requires a non-empty insert. The current "replace with a space and clean up later" workaround documented in user notes wouldn't be needed if the CLI rejected the empty case at parse time.

Related

  • Issue Bug: docs find-replace --format markdown silently does nothing #494 (closed) — different silent-failure bug in find-replace --format markdown (no replacement at all). Not the same root cause.
  • The CLAUDE.md / user-notes warning ("DANGER: gog docs find-replace --dry-run is broken") was written after the 2026-03-25 LoE incident. The bug was never fixed; today's testing confirms it is still live in v0.11.0-33.

Workaround

Until fixed: never pass --dry-run to gog docs find-replace. Test on a copy of the document instead. The /gws skill in my Claude Code setup explicitly forbids the --dry-run --content-file combination on this command.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions