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.
br updatesilently replaces non-empty text fields with no guardVersion: br 0.5.7
Summary
Every
br updatetext-field flag performs a blind full-field overwrite. There isno 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 aper-item operation, but it is not a distinct bug — it is the general behaviour
with a misleading name attached.
Reproduction — three fields, same result
All three fields are replaced silently. Recovery requires restoring from a
.beads/.br_history/issues.*.jsonlsnapshot or rewriting by hand.The
issuestable carries further free-text columns that appear to share thebehaviour —
design,close_reason,delete_reason,agent_context,external_ref,source_repo_path.designandagent_contextmatter most inpractice: they accumulate context across an issue's life and are exactly the
fields nobody expects a single flag to wipe.
The
--acceptancealias makes it worse, but is not the causebr update --helplists--acceptanceand--acceptance-criteriaas aliases ofthe same operation. There is no "check the Nth box" behaviour.
--acceptance 1reads 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:
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 isthe 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 updatethat wouldreplace a non-empty text field requires an explicit
--force, or refuses witha 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>.--acceptancereads as a per-item operation andis not one.
Optional, separable — add the operation the name implies. A
--check-acceptance <N>that marks the Nth item- [x]and leaves the rest intactwould 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 guardthat 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)
acceptance_criteria_uncheckedclose gate produces false positives forissues 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_criteriacolumn alone.br gate reportupdatesgate_result_historybut leaves the current-stategate_resultstable empty. Filed separately.--bypass-policyrequires a non-empty--bypass-reason; the binary's stringsconfirm the invariant, but the requirement is not obvious from
--help.