gastownhall/beads · error
legacy SQLite source %q must not be a symlink
Error message
legacy SQLite source %q must not be a symlink
What it means
fingerprintFile uses os.Lstat and explicitly rejects symlinks before opening any file that is part of the legacy SQLite source set. The library audits an exact on-disk layout, and a symlink could silently redirect reads to a different file (or be swapped between lstat and open). If the database file (or a required sidecar being fingerprinted) is a symlink, Export fails immediately with this message.
Source
Thrown at internal/migration/legacysqlite/reader.go:190
return sourceSet{}, err
}
journal, err := fingerprintFile(path+"-journal", false)
if err != nil {
return sourceSet{}, err
}
return sourceSet{db, wal, journal}, nil
}
func fingerprintFile(path string, required bool) (fingerprint, error) {
info, err := os.Lstat(path)
if os.IsNotExist(err) && !required {
return fingerprint{}, nil
}
if err != nil {
return fingerprint{}, err
}
if info.Mode()&os.ModeSymlink != 0 {
return fingerprint{}, fmt.Errorf("legacy SQLite source %q must not be a symlink", path)
}
if !info.Mode().IsRegular() {
return fingerprint{}, fmt.Errorf("legacy SQLite source %q must be a regular file", path)
}
f, err := os.Open(path) //nolint:gosec // G304: source is lstat-checked and fingerprinted again after sealing.
if err != nil {
return fingerprint{}, err
}
defer f.Close()
h := sha256.New()
if _, err = io.Copy(h, f); err != nil {
return fingerprint{}, err
}
return fingerprint{true, info.Size(), info.ModTime(), hex.EncodeToString(h.Sum(nil)), info}, nil
}
func sameSet(a, b sourceSet) bool {
return sameFingerprint(a.db, b.db) && sameFingerprint(a.wal, b.wal) && sameFingerprint(a.journal, b.journal)View on GitHub (pinned to 71377f2769)
Solutions
- Replace the symlink with the real file: rm the link and cp the target to the same path (keep -wal/-shm/-journal next to it)
- Point Export directly at the real file path instead of the symlink
- If you need indirection, bind-mount or copy the file into place rather than symlinking
Example fix
// before $ ls -l beads.db lrwxr-xr-x beads.db -> ~/sync/beads.db // after $ rm beads.db && cp ~/sync/beads.db beads.db && rm -f beads.db-wal beads.db-shm # only if copies, not moving live sidecars $ bd migrate --legacy ./beads.db ...
Defensive patterns
Strategy: validation
Validate before calling
func requireRegularNonSymlink(path string) error {
info, err := os.Lstat(path)
if err != nil { return err }
if info.Mode()&os.ModeSymlink != 0 {
return fmt.Errorf("%s is a symlink; replace with a real file", path)
}
if !info.Mode().IsRegular() {
return fmt.Errorf("%s is not a regular file", path)
}
return nil
} Type guard
func isRegularFile(path string) bool {
info, err := os.Lstat(path)
return err == nil && info.Mode().IsRegular() && info.Mode()&os.ModeSymlink == 0
} Try / catch
if err := isRegularFileErr(src); err != nil {
return fmt.Errorf("fix source path before export: %w", err)
}
return legacysqlite.Export(ctx, src, out, os.Stdout) Prevention
- Store beads.db as a real file, not a symlink into dotfiles/sync repos
- Resolve symlinks in scripts before invoking export
- Check `ls -l` on the source path when configuring migration jobs
- Use bind mounts or copies, not symlinks, for relocated data volumes
When it happens
Trigger: Export -> seal -> fingerprintSource -> fingerprintFile on the path given as the legacy source (the .db itself, or -wal/-shm/-journal when present), and os.Lstat reports os.ModeSymlink. Note the .db path is 'required', so this fires even if it is a dangling symlink (Lstat succeeds on the link itself).
Common situations: User keeps beads.db as a symlink into a synced folder or dotfiles repo; packaging scripts link the database from a data volume; container setups symlink /data/beads.db to a mounted path.
Related errors
- --output must not alias legacy SQLite source or sidecar
- sealed legacy SQLite database does not match source fingerpr
- sealed legacy SQLite WAL does not match source fingerprint
- legacy SQLite source changed while sealing
- legacy SQLite source %q must be a regular file
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/5fe5490bc14b3ec5.
Report an issue: GitHub.