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
- Choose a different --output file (e.g. issues.jsonl) that is not the database or its sidecars
- Remove any symlink/hard link at the output path that points to the database
- Write to stdout with output "-" and redirect: `bd ... - > issues.jsonl`
- 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
- Never set --output equal to the database or its -wal/-shm/-journal sidecars
- Check for symlinks/hard links at the output path that could alias the source (stat --format='%i')
- Use an explicit, dedicated output filename in scripts and CI
- Write to stdout ('-') and redirect when unsure
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
- legacy SQLite source %q must not be a symlink
- --source-db and --output are required
- reading confirmation: %w
- sealed legacy SQLite database does not match source fingerpr
- sealed legacy SQLite WAL does not match source fingerprint
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/46b866b31f88ab40.
Report an issue: GitHub.