gastownhall/beads · error

--output must not alias legacy SQLite source or sidecar

Error message

--output must not alias legacy SQLite source or sidecar

What it means

Export writes its output via a temp spool that is renamed to the --output path; if --output resolves to the same file (by canonical path or inode) as the legacy source database or one of its sidecars (-wal, -shm, -journal), the rename would destroy the source being migrated. rejectAlias detects this aliasing — including via symlinks and hard links (os.SameFile) — and fails before any data is read.

Source

Thrown at internal/migration/legacysqlite/reader.go:234

		return err
	}
	defer in.Close()
	out, err := os.OpenFile(to, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600) //nolint:gosec // G304: to is inside Export's private sealing directory.
	if err != nil {
		return err
	}
	_, err = io.Copy(out, in)
	closeErr := out.Close()
	if err != nil {
		return err
	}
	return closeErr
}

func rejectAlias(source, output string) error {
	for _, protected := range []string{source, source + "-wal", source + "-shm", source + "-journal"} {
		if samePath(protected, output) {
			return fmt.Errorf("--output must not alias legacy SQLite source or sidecar")
		}
	}
	return nil
}

func samePath(a, b string) bool {
	aa, errA := canonicalPath(a)
	bb, errB := canonicalPath(b)
	if errA != nil || errB != nil {
		return false
	}
	if aa == bb {
		return true
	}
	ai, errA := os.Stat(aa)
	bi, errB := os.Stat(bb)
	return errA == nil && errB == nil && os.SameFile(ai, bi)
}

View on GitHub (pinned to 71377f2769)

Solutions

  1. Choose a different --output file (e.g. issues.jsonl) that is not the database or its sidecars
  2. Remove any symlink/hard link at the output path that points to the database
  3. Write to stdout with output "-" and redirect: `bd ... - > issues.jsonl`
  4. Check the resolved path with readlink -f / stat --format='%i' to confirm output and source differ

Example fix

// before
$ bd migrate --legacy beads.db --output beads.db
// error: --output must not alias legacy SQLite source or sidecar
// after
$ bd migrate --legacy beads.db --output issues.jsonl
Defensive patterns

Strategy: validation

Validate before calling

func outputsDiffer(source, output string) error {
	if output == "-" { return nil }
	for _, p := range []string{source, source+"-wal", source+"-shm", source+"-journal"} {
		a, _ := filepath.Abs(output); b, _ := filepath.Abs(p)
		if filepath.Clean(a) == filepath.Clean(b) {
			return fmt.Errorf("--output %q aliases legacy source/sidecar", output)
		}
	}
	return nil
}
// call before Export: outputsDiffer(src, out)

Try / catch

if err := legacysqlite.Export(ctx, src, out, os.Stdout); err != nil {
	if strings.Contains(err.Error(), "must not alias legacy SQLite source") {
		return fmt.Errorf("choose a distinct --output path (e.g. issues.jsonl)")
	}
	return err
}

Prevention

When it happens

Trigger: Export(ctx, source, output, ...) with output != "-" and samePath(protected, output) true for source, source+"-wal", source+"-shm", or source+"-journal". samePath canonicalizes both paths (abs + EvalSymlinks) and also compares by inode, so relative paths, symlinks, and hard links all count as aliases.

Common situations: CLI misuse like `bd export --legacy beads.db --output beads.db`; setting output to beads.db-wal or -shm by glob/script accident; output is a symlink pointing at the database; hard-linked copies of the same inode.

Related errors


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