JuliusBrussee/caveman · error

file changed while opening

Error message

file changed while opening

What it means

chmodSQLiteFile opens the SQLite database file to tighten its mode to 0600 and, before doing so, verifies that the file it just opened is the same file that was stat'ed earlier (os.SameFile on device/inode identity). If they differ, the file was replaced, renamed, or recreated between the initial check and the open, so the chmod is aborted to avoid changing permissions on the wrong file. It is a TOCTOU (time-of-check-to-time-of-use) guard on the CCR store's Windows path.

Solutions

  1. Retry the operation; if the file was transiently replaced, a second attempt typically sees a stable file and passes
  2. Ensure only one engine instance uses the same CCR database path at a time (stop other processes/agents sharing the profile)
  3. Exclude the CCR directory from file-sync clients (OneDrive/Dropbox) and antivirus real-time replacement so the db file is not swapped during open
  4. If the file was intentionally recreated, re-run initialization against the fresh file rather than reusing the stale FileInfo

Example fix

// before: two processes racing on the same store
engine.New(..., StorePath: sharedPath) // in process A and B concurrently
// after: serialize or isolate the store path
// process A
mu.Lock(); defer mu.Unlock(); engine.New(..., StorePath: sharedPath)
// or give each process its own store
engine.New(..., StorePath: perProcessPath)
Defensive patterns

Strategy: retry

Validate before calling

// Before initializing the store, verify the db path is stable and not in a synced folder
info1, err := os.Stat(ccrPath)
if err == nil {
  time.Sleep(50 * time.Millisecond)
  info2, err2 := os.Stat(ccrPath)
  if err2 != nil || !os.SameFile(info1, info2) {
    return fmt.Errorf("ccr db path %s is being replaced concurrently", ccrPath)
  }
}

Try / catch

for attempt := 0; attempt < 3; attempt++ {
  err := store.Put(ctx, obj)
  if err != nil && strings.Contains(err.Error(), "file changed while opening") {
    time.Sleep(100 * time.Millisecond << attempt) // backoff and retry
    continue
  }
  return err
}
return fmt.Errorf("ccr db keeps changing during open; check for concurrent writers or sync clients")

Prevention

When it happens

Trigger: chmodSQLiteFile is invoked during store setup and the DB file at the given path is deleted and recreated, replaced by a new file, or swapped (e.g. by a concurrent process running maintenance/restore, a sync client replacing the file, or another engine instance reinitializing the store) between the initial Stat and the open.

Common situations: Two engine processes starting simultaneously against the same ~/.caveman/ccr.db; OneDrive/Dropbox sync tools replacing the db file during startup; a cleanup script or antivirus quarantine/recreate cycle touching the CCR file mid-initialization.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/7f251b71e90f5bd9. Report an issue: GitHub.

Appendix: source

Thrown at engine/ccr/sqlite_parent_security_windows.go:105

		return err
	}
	return file.Close()
}

func chmodSQLiteFile(path string, info os.FileInfo) error {
	// Windows locks are handle-based, so closing this separate descriptor does
	// not release the locks held by SQLite.
	file, err := os.OpenFile(path, os.O_RDWR, 0)
	if err != nil {
		return err
	}
	defer file.Close()
	opened, err := file.Stat()
	if err != nil {
		return err
	}
	if !os.SameFile(info, opened) {
		return fmt.Errorf("file changed while opening")
	}
	return file.Chmod(0o600)
}

View on GitHub (pinned to 3ee70a1026)