{"record":{"id":"5fe5490bc14b3ec5","repo":"gastownhall/beads","slug":"legacy-sqlite-source-q-must-not-be-a-symlink","errorCode":null,"errorMessage":"legacy SQLite source %q must not be a symlink","messagePattern":"legacy SQLite source %q must not be a symlink","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/migration/legacysqlite/reader.go","lineNumber":190,"sourceCode":"\t\treturn sourceSet{}, err\n\t}\n\tjournal, err := fingerprintFile(path+\"-journal\", false)\n\tif err != nil {\n\t\treturn sourceSet{}, err\n\t}\n\treturn sourceSet{db, wal, journal}, nil\n}\n\nfunc fingerprintFile(path string, required bool) (fingerprint, error) {\n\tinfo, err := os.Lstat(path)\n\tif os.IsNotExist(err) && !required {\n\t\treturn fingerprint{}, nil\n\t}\n\tif err != nil {\n\t\treturn fingerprint{}, err\n\t}\n\tif info.Mode()&os.ModeSymlink != 0 {\n\t\treturn fingerprint{}, fmt.Errorf(\"legacy SQLite source %q must not be a symlink\", path)\n\t}\n\tif !info.Mode().IsRegular() {\n\t\treturn fingerprint{}, fmt.Errorf(\"legacy SQLite source %q must be a regular file\", path)\n\t}\n\tf, err := os.Open(path) //nolint:gosec // G304: source is lstat-checked and fingerprinted again after sealing.\n\tif err != nil {\n\t\treturn fingerprint{}, err\n\t}\n\tdefer f.Close()\n\th := sha256.New()\n\tif _, err = io.Copy(h, f); err != nil {\n\t\treturn fingerprint{}, err\n\t}\n\treturn fingerprint{true, info.Size(), info.ModTime(), hex.EncodeToString(h.Sum(nil)), info}, nil\n}\n\nfunc sameSet(a, b sourceSet) bool {\n\treturn sameFingerprint(a.db, b.db) && sameFingerprint(a.wal, b.wal) && sameFingerprint(a.journal, b.journal)","sourceCodeStart":172,"sourceCodeEnd":208,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/migration/legacysqlite/reader.go#L172-L208","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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"],"exampleFix":"// before\n$ ls -l beads.db\nlrwxr-xr-x beads.db -> ~/sync/beads.db\n// after\n$ rm beads.db && cp ~/sync/beads.db beads.db && rm -f beads.db-wal beads.db-shm  # only if copies, not moving live sidecars\n$ bd migrate --legacy ./beads.db ...","handlingStrategy":"validation","validationCode":"func requireRegularNonSymlink(path string) error {\n\tinfo, err := os.Lstat(path)\n\tif err != nil { return err }\n\tif info.Mode()&os.ModeSymlink != 0 {\n\t\treturn fmt.Errorf(\"%s is a symlink; replace with a real file\", path)\n\t}\n\tif !info.Mode().IsRegular() {\n\t\treturn fmt.Errorf(\"%s is not a regular file\", path)\n\t}\n\treturn nil\n}","typeGuard":"func isRegularFile(path string) bool {\n\tinfo, err := os.Lstat(path)\n\treturn err == nil && info.Mode().IsRegular() && info.Mode()&os.ModeSymlink == 0\n}","tryCatchPattern":"if err := isRegularFileErr(src); err != nil {\n\treturn fmt.Errorf(\"fix source path before export: %w\", err)\n}\nreturn legacysqlite.Export(ctx, src, out, os.Stdout)","preventionTips":["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"],"tags":["sqlite","migration","symlink","path-safety"],"backgroundTag":"symlinked-database-path","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}