Skip to content

br update silently replaces non-empty text fields with no guard #467

Description

@Gerry9000

br update silently replaces non-empty text fields with no guard

Version: br 0.5.7

Summary

Every br update text-field flag performs a blind full-field overwrite. There is
no confirmation, no diff, and no warning, regardless of how much content is being
destroyed. A multi-line field built up over an issue's life is replaced by a
single-character argument without comment.

--acceptance <N> is the most visible instance because its name implies a
per-item operation, but it is not a distinct bug — it is the general behaviour
with a misleading name attached.

Reproduction — three fields, same result

ID=$(br create "field overwrite test" -t task -p 3 -d $'line one\nline two\nline three')

# description: 28 chars -> 1
br update $ID -d "1"
sqlite3 .beads/*.db "select length(description), description from issues where id='$ID'"
# -> 1|1

# notes: 20 chars -> 1
br update $ID --notes $'note A\nnote B\nnote C'
br update $ID --notes "1"
sqlite3 .beads/*.db "select length(notes), notes from issues where id='$ID'"
# -> 1|1

# acceptance_criteria: a completed 3-item block -> 1
br update $ID --acceptance-criteria $'- [x] AC1\n- [x] AC2\n- [x] AC3'
br update $ID --acceptance 1
sqlite3 .beads/*.db "select acceptance_criteria from issues where id='$ID'"
# -> 1

All three fields are replaced silently. Recovery requires restoring from a
.beads/.br_history/issues.*.jsonl snapshot or rewriting by hand.

The issues table carries further free-text columns that appear to share the
behaviour — design, close_reason, delete_reason, agent_context,
external_ref, source_repo_path. design and agent_context matter most in
practice: they accumulate context across an issue's life and are exactly the
fields nobody expects a single flag to wipe.

The --acceptance alias makes it worse, but is not the cause

br update --help lists --acceptance and --acceptance-criteria as aliases of
the same operation. There is no "check the Nth box" behaviour. --acceptance 1
reads as "mark acceptance item 1" and instead replaces the whole field with the
string 1.

Renaming or removing the alias would not fix the underlying problem, because a
typo or an unset shell variable destroys the same field just as quietly:

br update $ID --acceptance-criteria "$UNSET_VAR"   # field becomes empty
br update $ID -d "$TRUNCATED_HEREDOC"              # description becomes partial

Why the default is wrong for these fields

These are accumulating fields. Descriptions get extended, acceptance criteria get
appended to and checked off, notes and design context build up across a long-lived
issue. They are rarely rewritten wholesale, and when they are, the author knows it.

A blind overwrite is a reasonable default for a short scalar like --title. It is
the wrong default for a field whose normal usage is additive, especially in an
agent-driven or scripted workflow where the argument is frequently a variable
rather than a literal.

Suggested fix

Primary — guard the write, at the field level. Any br update that would
replace a non-empty text field requires an explicit --force, or refuses with
a message naming the field, its current length, and the override flag. Applying
this uniformly across the text fields covers unset variables, truncated
here-strings, typos, and the --acceptance <N> case with one rule.

Secondary — remove the misleading alias. Document only
--acceptance-criteria <TEXT>. --acceptance reads as a per-item operation and
is not one.

Optional, separable — add the operation the name implies. A
--check-acceptance <N> that marks the Nth item - [x] and leaves the rest intact
would be genuinely useful, but it should not gate the safety fix above.

Not recommended: inferring intent from the argument's shape — for example,
refusing when a value contains a digit but no - [ markdown. A data-loss guard
that works most of the time is worse than one that always asks, and it would miss
the unset-variable case entirely, which is the most common way this fires in
scripts.

Other findings from the same session (context for triage, not part of this report)

  • An acceptance_criteria_unchecked close gate produces false positives for
    issues whose comment bodies mention criterion labels in prose. The gate appears
    to read markdown from a merged source (the acceptance field plus comments)
    rather than the acceptance_criteria column alone.
  • br gate report updates gate_result_history but leaves the current-state
    gate_results table empty. Filed separately.
  • --bypass-policy requires a non-empty --bypass-reason; the binary's strings
    confirm the invariant, but the requirement is not obvious from --help.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions