← writing

Does beads store issues in git? Not on your branches any more

Search still says beads keeps issues in a JSONL file in git. In bd 1.3.1 they live in Dolt. We ran five cases to see what that does to branches.

Ask a search engine whether beads keeps its issues in git and it says yes: a .beads/issues.jsonl file, committed with your code, merged by a custom driver. The pages it answers from are package docs for beads 0.9 to 0.17.

That was true once. It is not how bd 1.3.1 works, the current release. Beads now keeps issues in Dolt, a version-controlled SQL database, and its changelog has called Dolt the primary datastore since 1.0.5. The JSONL file is an export, off by default, and the README says it is "not the source of truth or a backup".

The data still travels through your git remote. It just does not travel on your branches. We ran five cases with plain git and the release binary to see what that changes in practice.

Where the issues are now

Three places, and only one of them is a file git tracks.

WhatWhereTracked by git?
The issues themselves.beads/embeddeddolt/, a local Dolt databaseno, it is in .beads/.gitignore
Their shared historyrefs/dolt/data on your git remotea ref, but not a branch you check out
The JSONL file.beads/issues.jsonlonly if you turn export.auto on

bd init also makes a commit of its own: 19 files in our run, among them an AGENTS.md, a CLAUDE.md, a .claude/settings.json, config for Cursor and Codex, and five git hooks, with core.hooksPath pointed at them. Those are in git. The issues are not.

Five cases

Each case is a fresh repository. Cases 3 to 5 use two clones, A and B, of one bare remote, standing in for two agents on two machines. They sync the way the docs say to: bd dolt push and bd dolt pull.

CaseWhat happenedResult
1. Status changed on a feature branchgit status stayed clean. Back on main, the issue still read in_progress. Deleting the branch changed nothingthe issue does not follow the branch
2. Plain git cloneno issue data in the clone; bd list said "no beads database found"works after bd bootstrap
3. A changes status, B changes priorityboth edits keptclean merge
4. A sets in_progress, B closes it latersettled by the later timestamp; both ended closedmerged, one edit dropped
5. A and B each create an issueboth issues presentclean merge

Cases 3 and 5 are the ones a git-file tracker gets wrong. Two agents touching one task, or both adding tasks, is where a Markdown or JSONL file in git conflicts. Beads merged both with no help. The hash IDs and the field-level merge do what the README claims.

The same field, changed twice

Case 4 is the one worth reading slowly.

Two agents disagree about one value. One says the task is in progress, the other closes it. Beads does not stop. It keeps whichever edit has the later updated_at and moves the close fields together. On the clone where the merge happens it prints:

Notice: auto-merged issue t-…; status, updated_at, closed_at, close_reason
settled last-write-wins (the older side's edit was superseded)

The clone whose edit lost prints Pull complete. and nothing else. Swap the roles (case 4b) and the later edit wins again: a closed task is open again because someone else touched it two seconds later.

This is a reasonable rule and it is written down in beads' own source. It is still a rule, and the losing agent is not told. Our first runs also found its edge. updated_at is shown to the second, and the source sends a row to a person when both timestamps are equal. Without a pause between the two edits, the same case came back once auto-merged and once as "merge conflicts in issues require operator resolution". Two agents editing within the same second can get either outcome.

What this means for branches

Case 1 is the real change, and whether it is a problem depends on how you use branches.

If an agent works on a branch that gets thrown away, its task changes stay. A task it closed is still closed on main. There is nothing to revert, because nothing was committed.

If you review work as a pull request, the task changes are not in the diff. git log will not show when a task was closed, or by whom. That history exists in Dolt (bd dolt log), next to git, not inside it.

If a new person clones the repository, they get no issues until they run bd bootstrap. A teammate who only uses git sees the hooks and the agent files, and none of the work.

The other side is that this is what beads is built to do. One live task list shared by every agent and every machine, whatever branch each one is on, is the point of "distributed graph issue tracker". A task tracker that branches with your code gives each branch its own copy of the truth, and then you have to merge it. Beads chose to keep one copy.

So, does beads store issues in git?

It stores them next to git. They live in a Dolt database, synced through a ref on the same remote as your code. In the 0.x versions the search results describe, it was a JSONL file in your commits. Since 1.0.5 the changelog calls that file an optional export, not "the canonical git-tracked source of truth".

If you want task state to move with a branch and show up in review, that is now a different tool. If you want one list that every agent sees at once, beads 1.x does that, and merged our two-agent cases without help.

FAQ

Can I still keep beads issues in a JSONL file in git?

You can export them there: bd config set export.auto true, and the pre-commit hook refreshes the file. The docs say not to treat it as sync, because JSONL import is upsert-only and cannot tell a deleted issue from one never exported.

Do I need a Dolt server?

No. The default is an embedded engine inside bd, with one writer at a time. Server mode exists for several concurrent writers.

Does beads still use a git merge driver?

Not for the issues in 1.3.1. Merging happens inside Dolt, when you run bd dolt pull. That is why case 1 shows a clean git status: git has nothing to merge.

Is a closed task reopened by a later edit?

In our case 4b, yes: one clone closed the task, the other set it to in progress two seconds later, and after sync both read in_progress. The later updated_at won.


kadence takes the other side of this trade. Its tasks are an append-only journal committed in your repository, so they move with branches and show up in review. That also means they must be merged, which its merge test covers. It is much newer and far less used than beads, and the honest status on the home page says so.

Where the numbers come from

  • Probe run 2026-10-08: `does beads store issues in git` returned six pkg.go.dev pages for beads versions 0.9.6 to 0.17.7, a keepcoding.io post, a beads-based repository, Algolia DocSearch's page for the repo and an unrelated commit; the engine's summary said issues are stored in .beads/issues.jsonl, committed to git, with a merge driver. Recorded in this site's repository, docs/search-log/2026-10-08.md
  • beads README at v1.3.1: 'Distributed graph issue tracker for AI agents, powered by Dolt'; embedded mode keeps data in .beads/embeddeddolt/, single writer; 'Cross-machine sync uses bd dolt push / bd dolt pull against refs/dolt/data on your git remote; .beads/issues.jsonl is an export for viewers and interchange, not the source of truth or a backup'; 'Zero Conflict: Hash-based IDs prevent merge collisions' — github.com/steveyegge/beads, read 2026-10-08 with gh
  • beads docs/core-concepts/sync-concepts.md at v1.3.1: 'Dolt stores issue history under refs/dolt/data, separate from source branches such as refs/heads/main'; fresh clones run bd bootstrap; 'JSONL import is upsert-only' — read 2026-10-08
  • beads CHANGELOG.md, 1.0.5 (2026-05-28): JSONL auto-export and auto-staging became opt-in; 'Dolt is the primary datastore', .beads/issues.jsonl 'is not the canonical git-tracked source of truth'. Latest release v1.3.1, published 2026-09-30 — read 2026-10-08
  • beads internal/storage/versioncontrolops/automerge.go at v1.3.1: a column only one side changed keeps that side's value; a column both sides changed to different values is settled last-write-wins by updated_at; a row goes to the operator when the two updated_at values are equal or unparseable — read 2026-10-08
  • Bench run 2026-10-08 with bd 1.3.1 (release binary for darwin arm64, SHA-256 matched the release checksums.txt) and git 2.39.5 (Apple Git-154), the remote a bare repository on disk, run twice. bd init made one commit of 19 files including AGENTS.md, CLAUDE.md, .claude/settings.json and five git hooks, and set core.hooksPath. Case 1: status set to in_progress on a feature branch; git status clean; after checking out main and deleting the branch the issue still read in_progress. Case 2: the remote held refs/dolt/data beside refs/heads/main; a plain git clone had no issue data and bd list printed 'no beads database found' until bd bootstrap. Case 3: A set status, B set priority on one issue; both kept after sync. Case 4: A set in_progress, B closed two seconds later; B's pull printed 'auto-merged ... settled last-write-wins (the older side's edit was superseded)', both ended closed, A's pull printed only 'Pull complete.' Case 4b, roles swapped: both ended in_progress. Case 5: each created an issue; both present. In earlier runs without the two-second pause, the same-field case came back once auto-merged and once with 'merge conflicts in issues require operator resolution; merge aborted'. Script and output in this site's repository: docs/search-log/bench/2026-10-08-beads-and-git-branches.sh and .txt