gastownhall/beads · error

symbolic ref cycle while resolving %q

Error message

symbolic ref cycle while resolving %q

What it means

During worktree removal, bd resolves a comparison ref to its terminal (non-symbolic) ref by repeatedly running `git symbolic-ref`. This error is thrown when that resolution revisits a ref it has already seen, i.e. the symbolic ref chain forms a loop instead of terminating at a concrete ref. Git itself permits self-referential symbolic refs, so bd detects the cycle defensively and refuses to proceed.

Source

Thrown at cmd/bd/worktree_cmd.go:1909

		selector:    selector,
		explicit:    explicit,
		ref:         ref,
		terminalRef: terminalRef,
		oid:         oid,
	}, nil
}

func resolveWorktreeTerminalRef(
	ctx context.Context,
	git *worktreeRemovalGit,
	executionRoot string,
	ref string,
) (string, error) {
	current := ref
	seen := make(map[string]struct{})
	for range 16 {
		if _, duplicate := seen[current]; duplicate {
			return "", fmt.Errorf("symbolic ref cycle while resolving %q", ref)
		}
		seen[current] = struct{}{}

		output, err := git.output(ctx, executionRoot, "symbolic-ref", "--quiet", current)
		if err == nil {
			next := strings.TrimSpace(string(output))
			if !strings.HasPrefix(next, "refs/") {
				return "", fmt.Errorf("symbolic ref %q resolves outside refs/: %q", current, next)
			}
			current = next
			continue
		}
		var exitError *exec.ExitError
		if errors.As(err, &exitError) && exitError.ExitCode() == 1 {
			return current, nil
		}
		return "", fmt.Errorf("failed to inspect symbolic ref %q: %w", current, err)
	}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Run `git symbolic-ref <ref>` repeatedly on the reported ref to find the loop and break it with `git update-ref` or `git symbolic-ref <ref> <real-target>` pointing at a concrete ref
  2. Delete the broken ref with `git update-ref -d <ref>` (or `git branch -D`) and recreate it correctly
  3. Run `git fsck` to check overall repository ref/object integrity
  4. If only bd's comparator is affected, pass a different healthy ref via `--merged-into <ref>`

Example fix

// broken: self-referential symbolic ref
$ git symbolic-ref refs/heads/feature refs/heads/feature
// after: point it at a concrete target or delete it
$ git symbolic-ref refs/heads/feature refs/heads/main
# or
$ git update-ref -d refs/heads/feature
Defensive patterns

Strategy: validation

Validate before calling

// detect a symbolic ref cycle before invoking bd
func hasRefCycle(ctx context.Context, run func(...string) ([]byte, error), ref string) bool {
	seen := map[string]bool{}
	cur := ref
	for i := 0; i < 16; i++ {
		if seen[cur] { return true }
		seen[cur] = true
		out, err := run("symbolic-ref", "--quiet", cur)
		if err != nil { return false } // terminal ref, no cycle
		cur = strings.TrimSpace(string(out))
	}
	return true
}

Type guard

func isTerminalRef(ref string, symbolicTarget func(string) (string, bool)) bool {
	_, isSymbolic := symbolicTarget(ref)
	return !isSymbolic
}

Try / catch

// Go: wrap the removal call and inspect the message
result, err := bd.WorktreeRemove(name)
if err != nil {
	if strings.Contains(err.Error(), "symbolic ref cycle") {
		// repair refs or abort
		return fmt.Errorf("repair refs/heads before removing worktree: %w", err)
	}
	return err
}

Prevention

When it happens

Trigger: Calling worktree removal/comparator logic (`pinWorktreeComparatorRef` -> `resolveWorktreeTerminalRef`) with a ref whose `git symbolic-ref` chain loops, e.g. refs/heads/foo -> refs/heads/bar -> refs/heads/foo, or a ref pointing at itself.

Common situations: Corrupted or hand-edited .git refs; a scripted tool accidentally created a self-pointing symbolic ref (e.g. `git symbolic-ref refs/heads/x refs/heads/x`); a shallow/partial clone with damaged ref storage; filesystem-level ref manipulation gone wrong.

Related errors


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