cmd/bd/init.go, cmd/bd/init_safety.go,
cmd/bd/init_safety_test.go, cmd/bd/init_safety_help.go, and
cmd/bd/dolt.go.
This document lives next to the ADRs and matches the structure of bd’s
error messages: each named refusal in bd init and bd dolt push/pull
has a labeled anchor here with step-by-step recovery instructions. The
bd dolt push/pull fork refusal deep-links its anchor directly; since
#5310 the bd init
refusals point at bd help init-safety, which links this document as a
whole — match those by exit code and symptom text.
See also: bd help init-safety, and
ADR 0002 — bd init safety invariants.
Table of contents
- init-force-refused —
bd init --force/--reinit-localrefused because origin has Dolt history - init-token-missing — a destructive re-init refused because
--destroy-tokenis missing or wrong - init-local-exists —
bd init --reinit-localrefused because local data already exists - init-missing-server-db —
bd initrefused because the configured server-side database is missing - pk-fork-refused —
bd dolt pull/pushrefused because a table has different primary keys in its common ancestor - re-clone-gotchas — two gotchas hit during manual re-clone recovery: damaged stores left inside
data_dir, and a fresh clone missing clone-local tables
init-force-refused
Exit code:10 (ExitRemoteDivergenceRefused)
Symptom
bd init --force (or --reinit-local) tells bd to bypass the local
data-safety guard, and only that guard: neither flag bypasses this refusal
or the missing server-side database refusal.
bd init --from-jsonl selects a local JSONL export as the source. But the
remote already has project history. Proceeding would create an orphan local
Dolt branch with no common ancestor on origin. The
next bd dolt push would either fail (no common ancestor) or — worse, if
force-pushed — destroy the team’s data.
Recovery paths
Pick the one that matches your intent.
1. You want to adopt the remote’s history (most common)
.beads/.
Your local state is ignored; the team’s history becomes yours.
If you set aside the old .beads/dolt instead of deleting it, or bd list
fails right after this with table not found: leases, see
re-clone-gotchas below before you do anything else.
2. You want to diagnose what went wrong before deciding
bd doctor walks the local + remote state and names concrete problems.
bd dolt status shows the Dolt-level view. Neither modifies anything.
3. You intentionally want to overwrite the remote’s history (destructive)
This is a cross-boundary operation that affects every collaborator. You need to pair the local-source init (--reinit-local or --from-jsonl)
with --discard-remote. In interactive mode bd will prompt for
confirmation; in non-interactive mode you must supply a --destroy-token.
See bd help init-safety for the token format.
After bd init --reinit-local --discard-remote, your next
bd dolt push must be a history-replacing push. Coordinate with your
team before doing this.
init-token-missing
Exit code:12 (ExitDestroyTokenMissing)
Symptom
bd requires --destroy-token in place of the interactive confirmation
it cannot prompt for.
Both destructive paths need the token, not just the cross-boundary one:
--discard-remote, which would discard the remote’s Dolt history.- plain
--reinit-localover existing local issues, which would destroy them (see init-local-exists).
1. Run interactively
Re-run in a TTY.bd init --reinit-local --discard-remote will prompt
you to type the destroy-token at confirmation time.
2. Supply the token explicitly (CI/automation)
The token format isDESTROY-<issue-prefix>. For a project whose issue
prefix is bd:
bd’s error messages.
init-local-exists
Exit code:11 (ExitLocalExistsRefused) interactively;
12 (ExitDestroyTokenMissing) non-interactively
Symptom
Interactive (TTY): you declined the typed destroy N issues confirmation.
This is the only path that exits 11.
12, not 11:
.beads/ has existing issues. bd init --reinit-local (or its
deprecated alias --force) would permanently destroy them, so bd demands
an explicit confirmation first: the typed prompt in a TTY, and a
--destroy-token when there is no TTY. This applies to --reinit-local on
its own — --discard-remote is not required to trigger it.
A plain bd init over an initialized workspace does not reach either code:
the local-safety guard refuses it with an ordinary error before this point.
Recovery paths
1. Export first, then proceed
issue-export.jsonl lets you re-import individual issues if needed. It is not
a full database backup; use bd backup when the Dolt database is healthy
enough to create a restorable backup before reinitializing.
2. Investigate why you hit this
If you did NOT expectbd init to be the right command here, run
bd doctor first — you may be looking at a server config issue that a
re-init won’t fix.
init-missing-server-db
Symptombd init:
.beads/metadata.json carries a project_id, or (under --reinit-local or
--force) a workspace that predates project_id still has a local Dolt data
directory, in which case the message cites that instead of metadata.json.
Yet its configured database is not on the server, or the server could not be
reached to confirm.
That is treated as a recovery situation, not a fresh clone, and the two are
otherwise indistinguishable from the local filesystem alone: this project’s own
database directory is absent either way, because it was never created here or
because it was lost with the local storage. bd init used to resolve the
ambiguity by assuming “fresh clone” and creating the database. When the
assumption was wrong, the result was a new, empty database sitting at the name
the real one used to occupy — the 2026-08-11 fleet-wide data loss.
bd init now refuses instead, and the refusal is not bypassed by
--force or --reinit-local. Those flags authorize destroying a database
that exists; they do not authorize inventing an empty one where the configured
database has gone missing.
1. Find out where the database went (do this first)
--data-dir
all present exactly like this — and none of them need a re-init. Fixing the
server brings the data back untouched.
2. Restore from an export
.beads/backup/ for a JSONL export you can import manually.
3. Create a fresh empty database at this name
Destructive to any unrecovered data. Only when you are certain no recoverable data exists:--recreate-missing is per-invocation only. It is deliberately never implied
by --force, --reinit-local, a config key, or an environment variable,
because it authorizes the one thing this guard exists to prevent.
The refusal names --recreate-missing but does not print this command: it also
fires when the server merely could not be reached, so it sends you through
steps 1 and 2 here instead of handing you a line to paste. See
ADR 0002 — Invariant 4.
Limits worth knowing
Whether a workspace meets this refusal depends on two pieces of local
evidence: whether this project’s own database directory
(<dolt-data-dir>/<database>/.dolt) exists, and whether metadata.json
carries a project_id (introduced by GH#2372). With the configured database
missing from the server, or the server unreachable:
Adding
--recreate-missing lifts every refusal in the last four rows,
including the third row’s “already initialized”. It changes nothing in the
first row, where this project’s database is still on local disk. In
shared-server mode the data dir is machine-global (see below), so the last two
rows rarely apply there.
project_idis also inherited by a fresh clone, because.beads/metadata.jsonis git-tracked by default. Cloning a beads-managed repo whose server-side database does not exist yet therefore meets this refusal rather than a plain first init. It fails in the safe direction — nothing is created silently, and no existing data can be stranded — and--recreate-missingabove is the supported way through it. This is settled, not pending:engdocs/adr/0004-missing-database-guard-signal.mdrecords why no purely-local signal can replaceproject_idhere (absence of local state cannot tell “never initialized here” from “initialized here, storage since lost”), and why--recreate-missingis the permanent resolution path rather than a stopgap.- Server mode only, and shared-server mode counts as server mode — the guard
tests for this project’s database directory under the resolved Dolt data
dir, not for the data dir itself, which in shared mode is the machine-global
~/.beads/shared-server/doltthat is created on demand. - Proxied-server mode does not reach the
--reinit-local/--forceguard — the--proxied-serverdispatch inrunInit(cmd/bd/init.go) returns before it. It does reach the plain-init guard, becauserunInitProxiedServercallscheckExistingBeadsData, so--recreate-missingworks under--proxied-servertoo: the flag is assigned beside its flag read, upstream of every dispatch inrunInit. Keep it there. It was previously assigned next to the guard it gates, which is downstream of the proxied dispatch, and the flag was therefore inert on that one route while the refusal still told the operator to pass it. bd bootstraphas its own separate mode-blind create path.
pk-fork-refused
Symptomin its common ancestor). bd follows the error
with a short version of the recovery recipe below.
Why this happens
The two histories being merged disagree about a table’s primary key set —
not about row contents. Dolt can cell-merge rows, but it refuses outright to
merge a table whose primary key was reshaped differently on each side (or
whose common ancestor had a different primary key than both sides). The
refusal happens before any row conflicts materialize, so bd dolt pull’s
conflict auto-resolver never gets a chance to run. Retrying never helps:
the histories are permanently un-mergeable.
The usual cause is upgrading bd independently on two clones while un-synced
changes existed on both sides, across a release whose schema migrations
reshape a primary key. Concretely: the
#4259 incident — clones
straddling the 0041/0043/0050 reshapes of dependencies (v1.0.4 →
v1.0.6) hit exactly this on the first post-upgrade pull if both sides had
unpushed dependency edits.
The remote-migrate prevention gate (v1.0.6+) exists to stop this from being
created: it refuses to auto-migrate a remote-backed database and tells you to
designate a single migrator. This playbook is for when the fork already
exists.
Recovery: bootstrap from one canonical clone
The forked histories cannot be merged, so one side must be chosen as
canonical and every other clone re-cloned from it. Issue data survives via
JSONL export/import; only the un-mergeable Dolt history is discarded on the
non-canonical clones.
1. Pick the canonical clone
Usually the most complete / most recently active clone. To compare, run on each clone (read-only):2. On the canonical clone: upgrade, migrate, force-push
bd’s migration gate may block here; that is exactly the designated-migrator
case the gate is asking about — follow the guidance it prints on the canonical
clone.)
3. On EVERY other clone: save local-only work, re-clone, re-apply
bd import has upsert semantics: issues that only existed on this clone are
re-created, newer local edits are applied, and rows older than what the
remote already has are skipped. Spot-check with bd stats afterwards.
Doing this by hand (moving .beads/dolt aside instead of rm -rf, or
skipping straight to bd list) can hit either of two live gotchas — see
re-clone-gotchas below.
Prevention (upgrades across PK-reshaping migrations)
- Sync before upgrading:
bd dolt push+bd dolt pullon every clone while all clones still run the old version, then stop editing. Once the new binary is installed,bd dolt push/bd dolt pullare gated too, so this must happen first. The one exception is the data-behind stop — a clone level on schema but missing commits the remote has — wherebd dolt pullis the remedy and is allowed through (a proxied-server workspace refuses it, so there the pull runs on the server host); the gate says so when it fires, and Clone behind the remote has the recipe. Plan for the rule, not the exception. - One designated migrator: upgrade one machine, let it migrate, then
bd dolt push. - Every other clone takes the migrated database, does not migrate: after
the migrator pushes, each other clone runs
bd dolt pullwhile still on the old binary, then upgrades. Once the new binary is installed that pull is refused while the clone has pending migrations, andbd bootstrapnever replaces an existing database — on its own it reportsNothing to do.— so a clone that upgraded first must move its local database aside beforebd bootstrapcan re-clone. The recipe is in Remote-backed databases and multiple clones.
re-clone-gotchas
Two gotchas hit during a live manual re-clone recovery (issue ga-vrq5pu), each of which cost real time because the symptom looks nothing like the cause. Both apply any time you set aside or replace a Dolt database directory by hand — during the pk-fork-refused playbook above, the init-force-refusedbd bootstrap path, or any other
manual re-clone.
Gotcha 1 — a damaged/set-aside store must go OUTSIDE data_dir
Symptomdata_dir (default
.beads/dolt/, overridable via BEADS_DOLT_DATA_DIR or the dolt_data_dir
field in metadata.json) as its own database and tries to load it. If you
move a damaged or superseded database directory aside but leave it inside
data_dir (for example, renaming .beads/dolt/mydb to
.beads/dolt/mydb.bak instead of moving it out of .beads/dolt/
entirely), the server tries to load the damaged copy too and dies on it —
even though the healthy database sitting right next to it is fine.
Check which mode you are in before you go looking for data_dir: in shared
server mode (BEADS_DOLT_SHARED_SERVER=1, or the dolt.shared-server config
key) it is ~/.beads/shared-server/dolt/ — or $BEADS_SHARED_SERVER_DIR/dolt/
— and that takes precedence over both of the per-project knobs above. That is
also where this gotcha bites hardest: one shared data_dir holds every
project’s database on the machine, so a single stray subdirectory crash-loops
the server for all of them.
The fix
When you set a database directory aside by hand, move it outside
data_dir — e.g. up to /tmp/ or a sibling of .beads/, never to a
sibling path still under .beads/dolt/.
Do not wait for an automated repair to do it for you: no bd doctor --fix
repair performs this move here. The two that come closest are:
- The corrupt-manifest repair renames the damaged database’s own
.dolt/directory in place —<data_dir>/<db>/.dolt→<data_dir>/<db>/.dolt.<ts>.corrupt.backup— and reinitializes beside it, so in the standard<data_dir>/<db>/layout the backup is nested inside the database directory rather than becoming a new direct child ofdata_dir. It also fires only once its scan can prove the store holds no recoverable chunk data, so it will not touch the superseded-but-populated copy this gotcha is about (internal/doltserver/manifest_recovery.go). - The rename that does move a whole
data_dirto a timestamped sibling (data_dir→data_dir.<ts>.corrupt.backup) belongs to a different repair, the database-integrity recovery (cmd/bd/doctor/fix/database_integrity.go) — and that one refuses outright for a repo configured in Dolt server mode, so if the crash-looping server is your configured backend it is unavailable too.
data_dir.
Gotcha 2 — a fresh clone needs bd migrate schema
Symptom
wisps, events,
local_metadata, or another clone-local table). A supervisor or agent
harness that expects to load session beads right after a fresh clone fails
here.
Why this happens
A handful of tables — leases, wisps, wisp_*, events, bd_events_*,
local_metadata, ignored_schema_migrations, repo_mtimes — are
dolt-ignored, clone-local tables: they exist on a running database but are
deliberately excluded from what bd dolt push/pull/clone transfers, so a
fresh clone starts without them.
A writable open normally re-materializes them on its own, whether it is
embedded or reaches the database through a server or proxied sql-server: it
runs the schema migration pass, which replays the clone-local (“ignored”)
series whenever that series’ cursor table is behind — and the cursor table,
ignored_schema_migrations, is itself clone-local, so a fresh clone always
qualifies (internal/storage/schema/schema.go,
internal/storage/uow/dolt_sql_provider.go). Reaching the error above
therefore means the open that hit it was not one of those self-healing
opens. The kinds that skip the pass, or only verify, include a deliberately
non-mutating read-only or preview open, a team-server open (it checks the
schema rather than migrating it), the preview-attach path, and any open whose
open-time migration gate refused. Run the migration explicitly to close the
gap.
The fix
--force needed. This replays the clone-local tables and prints:
bd bootstrap, before relying on bd list or any other command that reads
session state.