{"record":{"id":"46b866b31f88ab40","repo":"gastownhall/beads","slug":"output-must-not-alias-legacy-sqlite-source-or-si","errorCode":null,"errorMessage":"--output must not alias legacy SQLite source or sidecar","messagePattern":"--output must not alias legacy SQLite source or sidecar","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/migration/legacysqlite/reader.go","lineNumber":234,"sourceCode":"\t\treturn err\n\t}\n\tdefer in.Close()\n\tout, 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.\n\tif err != nil {\n\t\treturn err\n\t}\n\t_, err = io.Copy(out, in)\n\tcloseErr := out.Close()\n\tif err != nil {\n\t\treturn err\n\t}\n\treturn closeErr\n}\n\nfunc rejectAlias(source, output string) error {\n\tfor _, protected := range []string{source, source + \"-wal\", source + \"-shm\", source + \"-journal\"} {\n\t\tif samePath(protected, output) {\n\t\t\treturn fmt.Errorf(\"--output must not alias legacy SQLite source or sidecar\")\n\t\t}\n\t}\n\treturn nil\n}\n\nfunc samePath(a, b string) bool {\n\taa, errA := canonicalPath(a)\n\tbb, errB := canonicalPath(b)\n\tif errA != nil || errB != nil {\n\t\treturn false\n\t}\n\tif aa == bb {\n\t\treturn true\n\t}\n\tai, errA := os.Stat(aa)\n\tbi, errB := os.Stat(bb)\n\treturn errA == nil && errB == nil && os.SameFile(ai, bi)\n}","sourceCodeStart":216,"sourceCodeEnd":252,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/migration/legacysqlite/reader.go#L216-L252","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\n$ bd migrate --legacy beads.db --output beads.db\n// error: --output must not alias legacy SQLite source or sidecar\n// after\n$ bd migrate --legacy beads.db --output issues.jsonl","handlingStrategy":"validation","validationCode":"func outputsDiffer(source, output string) error {\n\tif output == \"-\" { return nil }\n\tfor _, p := range []string{source, source+\"-wal\", source+\"-shm\", source+\"-journal\"} {\n\t\ta, _ := filepath.Abs(output); b, _ := filepath.Abs(p)\n\t\tif filepath.Clean(a) == filepath.Clean(b) {\n\t\t\treturn fmt.Errorf(\"--output %q aliases legacy source/sidecar\", output)\n\t\t}\n\t}\n\treturn nil\n}\n// call before Export: outputsDiffer(src, out)","typeGuard":null,"tryCatchPattern":"if err := legacysqlite.Export(ctx, src, out, os.Stdout); err != nil {\n\tif strings.Contains(err.Error(), \"must not alias legacy SQLite source\") {\n\t\treturn fmt.Errorf(\"choose a distinct --output path (e.g. issues.jsonl)\")\n\t}\n\treturn err\n}","preventionTips":["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"],"tags":["sqlite","migration","cli","path-safety","aliasing"],"backgroundTag":"output-path-aliases-source","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}