gastownhall/beads · warning

git index is locked at %s; skipping auto-stage

Error message

git index is locked at %s; skipping auto-stage

What it means

gitAddFile runs a preflight that resolves the git index lock path (via `git rev-parse --git-dir`) and checks whether index.lock exists. If the lock file is present, it skips staging and returns this error so auto-export does not fight a concurrent git operation. It is a deliberate skip, not a crash of the export itself.

Source

Thrown at cmd/bd/export_auto.go:849

}

// gitAddFile stages a file in the enclosing git repo. When called from
// inside a git hook, it scrubs inherited GIT_* env vars (so git
// rediscovers the repo from cwd rather than treating cmd.Dir as the
// worktree root) and skips staging when the target is outside the hook's
// worktree (the .beads/redirect case, where staging would pollute the
// main repo's index). See GH#3311, scrubGitHookEnv, hookWorkTreeRoot.
func gitAddFile(path string) error {
	if wt := hookWorkTreeRoot(); wt != "" && !pathInsideDir(path, wt) {
		// Running inside a hook AND target is outside the hook's worktree.
		// Staging here would pollute a different repo's index; skip.
		return nil
	}

	env := scrubGitHookEnv(os.Environ())
	if lockPath, err := gitIndexLockPath(path, env); err == nil && lockPath != "" {
		if _, statErr := os.Stat(lockPath); statErr == nil {
			return fmt.Errorf("git index is locked at %s; skipping auto-stage", lockPath)
		} else if !os.IsNotExist(statErr) {
			return fmt.Errorf("failed to check git index lock %s: %w", lockPath, statErr)
		}
	} else if err != nil {
		debug.Logf("auto-export: git add lock preflight skipped: %v\n", err)
	}

	ctx, cancel := context.WithTimeout(context.Background(), gitAddTimeout)
	defer cancel()
	// Pass the basename only, defensively: cmd.Dir is the parent of path, so
	// a full path argument would double-root (cd .beads && git add
	// .beads/issues.jsonl → pathspec looks under .beads/.beads/) if a caller
	// ever passed a relative path here. Both current callers pass absolute
	// paths, so this guards against a regression rather than fixing a live
	// failure. See GH#4351.
	// Keep cmd.Dir = parent so GH#3311 hook worktree staging still resolves
	// the index path under the repo root (not bare "issues.jsonl" at root).
	cmd := exec.CommandContext(ctx, "git", "add", "--", filepath.Base(path))

View on GitHub (pinned to 71377f2769)

Solutions

  1. Wait for the concurrent git operation to finish and let the next auto-export stage the file
  2. If no git process is running, remove the stale lock: `rm -f .git/index.lock` (or the path shown in the error), then retry
  3. Run `bd sync` / explicit export to force staging once the lock clears

Example fix

// before
// git index is locked at /repo/.git/index.lock; skipping auto-stage
// after
rm -f /repo/.git/index.lock   # only if no git process is actually running
bd sync
Defensive patterns

Strategy: retry

Validate before calling

// Preflight: check for a live index.lock before exporting
if _, err := os.Stat(filepath.Join(gitDir, "index.lock")); err == nil {
    fmt.Println("git index locked; wait for the git operation to finish")
}

Try / catch

if err := autoExport(); err != nil && strings.Contains(err.Error(), "index is locked") {
    time.Sleep(2 * time.Second)
    err = autoExport() // retry after the concurrent git op likely finished
}

Prevention

When it happens

Trigger: gitAddFile (called from maybeAutoExport during auto-export) finds an existing index.lock in the resolved git dir at the moment it tries to stage the JSONL — typically while another git process (commit, rebase, another bd hook) holds the index.

Common situations: A git GUI, IDE, or parallel `git commit` is running when bd's post-hook fires; a stale index.lock left by a crashed git process; two bd instances auto-exporting concurrently.

Related errors


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/241525411c46a6c5. Report an issue: GitHub.