Files
beads/docs/PROTECTED_BRANCHES.md
Peter Chanthamynavong 2cbf3a5497 fix: Validate sync-branch at config-time and runtime (closes #1166) (#1168)
* fix(config): validate sync-branch at config time

Add sync-branch validation to validateYamlConfigValue() to reject
main/master at config time, preventing the validation bypass in GH#1166.

- Add case for sync-branch and sync.branch keys
- Inline validation logic to avoid import cycle (config <-> syncbranch)
- Add unit tests for rejection (main/master) and acceptance (valid names)

Part of: #1166

* fix(sync): add runtime guard for sync-branch == current-branch

Add dynamic runtime check before worktree operations to catch cases
where sync-branch matches the current branch. This provides defense
in depth for manual YAML edits, pre-fix configs, or non-main/master
branch names (trunk, develop, production, etc.).

- Check IsSyncBranchSameAsCurrent() after hasSyncBranchConfig is set
- Position check BEFORE worktree entry (CWD changes inside worktree)
- Add integration test TestSync_FailsWhenOnSyncBranch

Part of: #1166

* docs: note main/master restriction in sync-branch FAQ

Clarifies that git worktrees cannot checkout the same branch in
multiple locations, so main/master cannot be used as sync branch.
2026-01-19 10:11:06 -08:00

17 KiB

Protected Branch Workflow

This guide explains how to use beads with protected branches on platforms like GitHub, GitLab, and Bitbucket.

Table of Contents

Overview

Problem: GitHub and other platforms let you protect branches (like main) to require pull requests for all changes. This prevents beads from auto-committing issue updates directly to main.

Solution: Beads can commit to a separate branch (like beads-sync) using git worktrees, while keeping your main working directory untouched. Periodically merge the metadata branch back to main via a pull request.

Benefits:

  • Works with any git platform's branch protection
  • Main branch stays protected
  • No disruption to your primary working directory
  • Backward compatible (opt-in via config)
  • Minimal disk overhead (uses sparse checkout)
  • Platform-agnostic solution

Quick Start

1. Initialize beads with a separate sync branch:

cd your-project
bd init --branch beads-sync

This creates a .beads/ directory and configures beads to commit to beads-sync instead of main.

Important: After initialization, you'll see some untracked files that should be committed to your protected branch:

# Check what files were created
git status

# Commit the beads configuration to your protected branch
git add .beads/.gitignore .gitattributes
git commit -m "Initialize beads issue tracker"
git push origin main  # Or create a PR if required

Files created by bd init --branch:

Files that should be committed to your protected branch (main):

  • .beads/.gitignore - Tells git what to ignore in .beads/ directory
  • .gitattributes - Configures merge driver for intelligent JSONL conflict resolution

Files that are automatically gitignored (do NOT commit):

  • .beads/beads.db - SQLite database (local only, regenerated from JSONL)
  • .beads/daemon.lock, daemon.log, daemon.pid - Runtime files
  • .beads/beads.left.jsonl, beads.right.jsonl - Temporary merge artifacts

The sync branch (beads-sync) will contain:

  • .beads/issues.jsonl - Issue data in JSONL format (committed automatically by daemon)
  • .beads/metadata.json - Metadata about the beads installation
  • .beads/config.yaml - Configuration template (optional)

2. Start the daemon with auto-commit:

bd daemon start --auto-commit

The daemon will automatically commit issue changes to the beads-sync branch.

3. When ready, merge to main:

# Check what's changed
bd sync --status

# Merge to main (creates a pull request or direct merge)
bd sync --merge

That's it! The complete workflow is described below.

How It Works

Git Worktrees

Beads uses git worktrees to maintain a lightweight checkout of your sync branch. Think of it as a mini git clone that shares the same repository history.

Directory structure:

your-project/
├── .git/                    # Main git directory
│   └── beads-worktrees/
│       └── beads-sync/  # Worktree (only .beads/ checked out)
│           └── .beads/
│               └── issues.jsonl
├── .beads/                  # Your main copy
│   ├── beads.db
│   ├── issues.jsonl
│   └── .gitignore
├── .gitattributes           # Merge driver config (in main branch)
└── src/                     # Your code (untouched)

What lives in each branch:

Main branch (protected):

  • .beads/.gitignore - Tells git what to ignore
  • .gitattributes - Merge driver configuration

Sync branch (beads-sync):

  • .beads/issues.jsonl - Issue data (committed by daemon)
  • .beads/metadata.json - Repository metadata
  • .beads/config.yaml - Configuration template

Not tracked (gitignored):

  • .beads/beads.db - SQLite database (local only)
  • .beads/daemon.* - Runtime files

Key points:

  • The worktree is in .git/beads-worktrees/ (hidden from your workspace)
  • Only .beads/ is checked out in the worktree (sparse checkout)
  • Changes to issues are committed in the worktree
  • Your main working directory is never affected
  • Disk overhead is minimal (~few MB for the worktree)

Automatic Sync

When you update an issue:

  1. Issue is updated in .beads/beads.db (SQLite database)
  2. Daemon exports to .beads/issues.jsonl (JSONL file)
  3. JSONL is copied to worktree (.git/beads-worktrees/beads-sync/.beads/)
  4. Daemon commits the change in the worktree to beads-sync branch
  5. Main branch stays untouched (no commits on main)

Setup

Option 1: Initialize New Project

cd your-project
bd init --branch beads-sync

This will:

  • Create .beads/ directory with database
  • Set sync.branch config to beads-sync
  • Import any existing issues from git (if present)
  • Prompt to install git hooks (recommended: say yes)

Option 2: Migrate Existing Project

If you already have beads set up and want to switch to a separate branch:

# Set the sync branch
bd config set sync.branch beads-sync

# Start the daemon (it will create the worktree automatically)
bd daemon start --auto-commit

Daemon Configuration

For automatic commits to the sync branch:

# Start daemon with auto-commit
bd daemon start --auto-commit

# Or with auto-commit and auto-push
bd daemon start --auto-commit --auto-push

Daemon modes:

  • --auto-commit: Commits to sync branch after each change
  • --auto-push: Also pushes to remote after each commit
  • Default interval: 5 seconds (check for changes every 5s)

Recommended: Use --auto-commit but not --auto-push if you want to review changes before pushing. Use --auto-push if you want fully hands-free sync.

Environment Variables

You can also configure the sync branch via environment variable:

export BEADS_SYNC_BRANCH=beads-sync
bd daemon start --auto-commit

This is useful for CI/CD or temporary overrides.

Daily Workflow

For AI Agents

AI agents work exactly the same way as before:

# Create issues
bd create "Implement user authentication" -t feature -p 1

# Update issues
bd update bd-a1b2 --status in_progress

# Close issues
bd close bd-a1b2 "Completed authentication"

All changes are automatically committed to the beads-sync branch by the daemon. No changes are needed to agent workflows!

For Humans

Check status:

# See what's changed on the sync branch
bd sync --status

This shows the diff between beads-sync and main (or your current branch).

Manual commit (if not using daemon):

bd sync --flush-only  # Export to JSONL and commit to sync branch

Pull changes from remote:

# Pull updates from other collaborators
bd sync --no-push

This pulls changes from the remote sync branch and imports them to your local database.

Merging Changes

For protected branches with required reviews:

# 1. Push your sync branch
git push origin beads-sync

# 2. Create PR on GitHub/GitLab/etc.
#    - Base: main
#    - Compare: beads-sync

# 3. After PR is merged, update your local main
git checkout main
git pull
bd import  # Import the merged changes

Option 2: Direct Merge (If Allowed)

If you have push access to main:

# Check what will be merged
bd sync --merge --dry-run

# Merge sync branch to main
bd sync --merge

# This will:
# - Switch to main branch
# - Merge beads-sync with --no-ff (creates merge commit)
# - Push to remote
# - Import merged changes to database

Safety checks:

  • Verifies you're not on the sync branch
  • Checks for uncommitted changes in working tree
  • Detects merge conflicts and provides resolution steps
  • Uses --no-ff for clear history

Merge Conflicts

If you encounter conflicts during merge:

# bd sync --merge will detect conflicts and show:
Error: Merge conflicts detected
Conflicting files:
  .beads/issues.jsonl

To resolve:
1. Fix conflicts in .beads/issues.jsonl
2. git add .beads/issues.jsonl
3. git commit
4. bd import  # Reimport to sync database

Resolving JSONL conflicts:

JSONL files are append-only and line-based, so conflicts are rare. When they occur:

  1. Open .beads/issues.jsonl and look for conflict markers (<<<<<<<, =======, >>>>>>>)
  2. Both versions are usually valid - keep both lines
  3. Remove the conflict markers
  4. Save and commit

Example conflict resolution:

<<<<<<< HEAD
{"id":"bd-a1b2","title":"Feature A","status":"closed","updated_at":"2025-11-02T10:00:00Z"}
=======
{"id":"bd-a1b2","title":"Feature A","status":"in_progress","updated_at":"2025-11-02T09:00:00Z"}
>>>>>>> beads-sync

Resolution: Keep the line with the newer updated_at:

{"id":"bd-a1b2","title":"Feature A","status":"closed","updated_at":"2025-11-02T10:00:00Z"}

Then:

git add .beads/issues.jsonl
git commit -m "Resolve issues.jsonl merge conflict"
bd import  # Import to database (will use latest timestamp)

Troubleshooting

"fatal: refusing to merge unrelated histories"

This happens if you created the sync branch independently. Merge with --allow-unrelated-histories:

git merge beads-sync --allow-unrelated-histories --no-ff

Or use bd sync --merge which handles this automatically.

"worktree already exists"

If the worktree is corrupted or in a bad state:

# Remove the worktree
rm -rf .git/beads-worktrees/beads-sync

# Prune stale worktree entries
git worktree prune

# Restart daemon (it will recreate the worktree)
bd daemon stop && bd daemon start

"branch 'beads-sync' not found"

The sync branch doesn't exist yet. The daemon will create it on the first commit. If you want to create it manually:

git checkout -b beads-sync
git checkout main  # Switch back

Or just let the daemon create it automatically.

"Cannot push to protected branch"

If the sync branch itself is protected:

  1. Option 1: Unprotect the sync branch (it's metadata, doesn't need protection)
  2. Option 2: Use --auto-commit without --auto-push, and push manually when ready
  3. Option 3: Use a different branch name that's not protected

Daemon won't start

Check daemon status and logs:

# Check status
bd daemon status

# View logs
tail -f ~/.beads/daemon.log

# Restart daemon
bd daemon stop && bd daemon start

Common issues:

  • Port already in use: Another daemon is running
  • Permission denied: Check .beads/ directory permissions
  • Git errors: Ensure git is installed and repository is initialized

Changes not syncing between clones

Ensure all clones are configured the same way:

# On each clone, verify:
bd config get sync.branch  # Should be the same (e.g., beads-sync)

# Pull latest changes
bd sync --no-push

# Check daemon is running
bd daemon status

FAQ

Do I need to configure anything on GitHub/GitLab?

No! This is a pure git solution that works on any platform. Just protect your main branch as usual.

Can I use a different branch name?

Yes! Use any branch name except main or master (git worktrees cannot checkout the same branch in multiple locations):

bd init --branch my-custom-branch
# or
bd config set sync.branch my-custom-branch

Can I change the branch name later?

Yes:

bd config set sync.branch new-branch-name
bd daemon stop && bd daemon start

The old worktree will remain (no harm), and a new worktree will be created for the new branch.

What if I want to go back to committing to main?

Unset the sync branch config:

bd config set sync.branch ""
bd daemon stop && bd daemon start

Beads will go back to committing directly to your current branch.

Does this work with multiple collaborators?

Yes! Each collaborator configures their own sync branch:

# All collaborators use the same branch
bd config set sync.branch beads-sync

Everyone's changes sync via the beads-sync branch. Periodically merge to main via PR.

How often should I merge to main?

This depends on your workflow:

  • Daily: If you want issue history in main frequently
  • Per sprint: If you batch metadata updates
  • As needed: Only when you need others to see issue updates

There's no "right" answer - choose what fits your team.

Can I review changes before merging?

Yes! Use bd sync --status to see what's changed:

bd sync --status
# Shows diff between beads-sync and main

Or create a pull request and review on GitHub/GitLab.

What about disk space?

Worktrees are very lightweight:

  • Sparse checkout means only .beads/ is checked out
  • Typically < 1 MB for the worktree
  • Shared git history (no duplication)

Can I delete the worktree?

Yes, but the daemon will recreate it. If you want to clean up permanently:

# Stop daemon
bd daemon stop

# Remove worktree
git worktree remove .git/beads-worktrees/beads-sync

# Unset sync branch
bd config set sync.branch ""

Does this work with bd sync?

Yes! bd sync works normally and includes special commands for the merge workflow:

  • bd sync --status - Show diff between branches
  • bd sync --merge - Merge sync branch to main
  • bd sync --merge --dry-run - Preview merge

Can AI agents merge automatically?

Not recommended! Merging to main is a deliberate action that should be human-reviewed, especially with protected branches. Agents should create issues and update them; humans should merge to main.

However, if you want fully automated sync:

# WARNING: This bypasses branch protection!
bd daemon start --auto-commit --auto-push
bd sync --merge  # Run periodically (e.g., via cron)

What if I forget to merge for a long time?

No problem! The sync branch accumulates all changes. When you eventually merge:

bd sync --merge

All accumulated changes will be merged at once. Git history will show the full timeline.

Can I use this with GitHub Actions or CI/CD?

Yes! Example GitHub Actions workflow:

name: Sync Beads Metadata

on:
  schedule:
    - cron: '0 0 * * *'  # Daily at midnight
  workflow_dispatch:     # Manual trigger

jobs:
  sync:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
        with:
          fetch-depth: 0  # Full history

      - name: Install bd
        run: |
          curl -fsSL https://raw.githubusercontent.com/steveyegge/beads/main/scripts/install.sh | bash

      - name: Pull changes
        run: |
          git fetch origin beads-sync
          bd sync --no-push

      - name: Merge to main (if changes)
        run: |
          if bd sync --status | grep -q 'ahead'; then
            bd sync --merge
            git push origin main
          fi

Note: Make sure the GitHub Action has write permissions to push to main.

Platform-Specific Notes

GitHub

Protected branch settings:

  1. Go to Settings → Branches → Add rule
  2. Branch name pattern: main
  3. Check "Require pull request before merging"
  4. Save

Create sync branch PR:

git push origin beads-sync
gh pr create --base main --head beads-sync --title "Update beads metadata"

GitLab

Protected branch settings:

  1. Settings → Repository → Protected Branches
  2. Branch: main
  3. Allowed to merge: Maintainers
  4. Allowed to push: No one

Create sync branch MR:

git push origin beads-sync
glab mr create --source-branch beads-sync --target-branch main

Bitbucket

Protected branch settings:

  1. Repository settings → Branch permissions
  2. Branch: main
  3. Check "Prevent direct pushes"

Create sync branch PR:

git push origin beads-sync
# Create PR via Bitbucket web UI

Advanced Topics

Multiple Sync Branches

You can use different sync branches for different purposes:

# Development branch
bd config set sync.branch beads-dev

# Production branch
bd config set sync.branch beads-prod

Switch between them as needed.

Syncing with Upstream

If you're working on a fork:

# Add upstream
git remote add upstream https://github.com/original/repo.git

# Fetch upstream changes
git fetch upstream

# Merge upstream beads-sync to yours
git checkout beads-sync
git merge upstream/beads-sync
bd import  # Import merged changes

Custom Worktree Location

By default, worktrees are in .git/beads-worktrees/. This is hidden and automatic. If you need a custom location, you'll need to manage worktrees manually (not recommended).

Migration Guide

From Direct Commits to Sync Branch

If you have an existing beads setup committing to main:

  1. Set sync branch:

    bd config set sync.branch beads-sync
    
  2. Restart daemon:

    bd daemon stop && bd daemon start
    
  3. Verify:

    bd config get sync.branch  # Should show: beads-sync
    

Future commits will go to beads-sync. Historical commits on main are preserved.

From Sync Branch to Direct Commits

If you want to stop using a sync branch:

  1. Unset sync branch:

    bd config set sync.branch ""
    
  2. Restart daemon:

    bd daemon stop && bd daemon start
    

Future commits will go to your current branch (e.g., main).


Need help? Open an issue at https://github.com/steveyegge/beads/issues