JuliusBrussee/caveman · error
sqlite parent must be a directory
Error message
sqlite parent %q must be a directory
What it means
After resolving symlinks, PrepareSQLitePathCanonical stats the resolved parent and requires it to be a directory. This error is thrown when the resolved parent path exists but is a regular file, socket, fifo, or other non-directory entry, so a SQLite database cannot live inside it.
Solutions
- Check the resolved parent with `file` / `os.Stat` and confirm what it actually is
- Correct the configured path so the parent component is a real directory
- Rename/remove the offending file at that path and recreate the directory with os.MkdirAll
- If the path was produced by string concatenation, fix the join so the db filename is not nested under a file path
Example fix
// before
ccr.Open("/data/ccr.db/store.db") // /data/ccr.db is a file
// after
ccr.Open("/data/ccr/store.db") // parent /data/ccr is a directory Defensive patterns
Strategy: validation
Validate before calling
func parentIsDir(dbPath string) error {
fi, err := os.Stat(filepath.Dir(dbPath))
if err != nil { return err }
if !fi.IsDir() { return fmt.Errorf("%s is not a directory", filepath.Dir(dbPath)) }
return nil
} Try / catch
if err := ccr.Open(dbPath); err != nil && strings.Contains(err.Error(), "must be a directory") {
// fix config or recreate the parent as a directory before retrying
return fmt.Errorf("bad ccr path config: %w", err)
} Prevention
- Configure the db directory, never a file path, as the parent
- Stat the parent once at config-load time
- Guard against backup/restore jobs replacing directories with files
When it happens
Trigger: Calling Open/PrepareSQLitePath with a path like /path/to/file.db/subdir/db.sqlite where an intermediate component is a file; pointing the db path at a location whose 'parent' is actually a file (e.g. configured path 'data.db/db.sqlite'); a mountpoint replaced a directory with a regular file.
Common situations: Config where the user set the db 'directory' option to the file path instead of its folder; a tar/backup restore replaced a directory with a file; container image layering where a file shadows an expected directory.
Related errors
- inspect sqlite parent ACL
- inspect sqlite parent
- sqlite parent has no restrictive DACL
- sqlite parent has no security descriptor
- sqlite parent is group/world writable
AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20).
Data as JSON: /api/errors/fa95ab31858e9f69.
Report an issue: GitHub.
Appendix: source
Thrown at engine/ccr/store_sqlite.go:301
}
if strings.TrimSpace(path) == "" {
return "", fmt.Errorf("ccr sqlite path is required")
}
parent := filepath.Dir(path)
absoluteParent, err := filepath.Abs(parent)
if err != nil {
return "", fmt.Errorf("resolve sqlite parent %q: %w", parent, err)
}
resolvedParent, err := filepath.EvalSymlinks(absoluteParent)
if err != nil {
return "", fmt.Errorf("resolve sqlite parent %q: %w", parent, err)
}
parentInfo, err := os.Stat(resolvedParent)
if err != nil {
return "", fmt.Errorf("inspect sqlite parent %q: %w", resolvedParent, err)
}
if !parentInfo.IsDir() {
return "", fmt.Errorf("sqlite parent %q must be a directory", resolvedParent)
}
if err := validateSQLiteParentSecurity(resolvedParent, parentInfo); err != nil {
return "", err
}
canonicalPath := filepath.Join(resolvedParent, filepath.Base(path))
if err := secureSQLiteFile(canonicalPath, true); err != nil {
return "", fmt.Errorf("secure sqlite %q: %w", canonicalPath, err)
}
for _, suffix := range []string{"-wal", "-shm"} {
if err := secureSQLiteFile(canonicalPath+suffix, false); err != nil {
return "", fmt.Errorf("secure sqlite sidecar %q: %w", canonicalPath+suffix, err)
}
}
return canonicalPath, nil
}
func secureSQLiteFiles(path string) error {
if path == ":memory:" {View on GitHub (pinned to 3ee70a1026)