gastownhall/beads · error
opening lock file: %w
Error message
opening lock file: %w
What it means
AcquireSyncLock opens (creating if needed) <beadsDir>/sync.lock with os.OpenFile(O_CREATE|O_RDWR, 0600) to hold the flock. If opening fails it wraps the OS error as "opening lock file: %w" and returns nil. This happens before any locking attempt, so it reflects file-creation problems rather than contention.
Source
Thrown at internal/linear/synclock.go:44
type SyncLockInfo struct {
PID int
Started time.Time
}
// AcquireSyncLock acquires the sync lock for the given beads directory.
// If wait is true, blocks until the lock is available. If false, returns
// an error immediately when the lock is held by another live process.
func AcquireSyncLock(beadsDir string, wait bool) (*SyncLock, error) {
lockPath := filepath.Join(beadsDir, syncLockFilename)
infoPath := syncLockMetadataPath(beadsDir, lockPath)
if err := os.MkdirAll(beadsDir, 0755); err != nil {
return nil, fmt.Errorf("creating beads directory: %w", err)
}
f, err := os.OpenFile(lockPath, os.O_CREATE|os.O_RDWR, 0600) // #nosec G304 -- lockPath is constrained to the beads directory.
if err != nil {
return nil, fmt.Errorf("opening lock file: %w", err)
}
if wait {
if err := lockfile.FlockExclusiveBlocking(f); err != nil {
_ = f.Close()
return nil, fmt.Errorf("acquiring lock (blocking): %w", err)
}
} else {
if err := lockfile.FlockExclusiveNonBlocking(f); err != nil {
if lockfile.IsLocked(err) || err == lockfile.ErrLockBusy {
info := readContendedSyncLockInfo(infoPath)
_ = f.Close()
return nil, &SyncLockHeldError{Info: info}
}
_ = f.Close()
return nil, fmt.Errorf("acquiring lock (non-blocking): %w", err)
}
}View on GitHub (pinned to 71377f2769)
Solutions
- Fix ownership/permissions on <beadsDir> and sync.lock so the running user can create/read-write (0600)
- Check file descriptor usage (ulimit -n, lsof) for leaked handles if the error is EMFILE
- Verify the filesystem is writable and not read-only mounted
- If a stale lock file from another user blocks you, remove it after confirming no sync process is running
Example fix
// before
lock, err := AcquireSyncLock(beadsDir, true)
// after: check writability first
if err := checkWritable(beadsDir); err != nil {
return fmt.Errorf("cannot acquire sync lock: %w", err)
}
lock, err := AcquireSyncLock(beadsDir, true) Defensive patterns
Strategy: validation
Validate before calling
// ensure directory is writable before acquiring
probe := filepath.Join(beadsDir, ".probe")
if err := os.WriteFile(probe, nil, 0600); err != nil {
return fmt.Errorf("beads dir not writable: %w", err)
}
os.Remove(probe)
lock, err := AcquireSyncLock(beadsDir, true) Type guard
null
Try / catch
lock, err := AcquireSyncLock(beadsDir, true)
if err != nil && strings.Contains(err.Error(), "opening lock file") {
return fmt.Errorf("cannot open sync lock in %s: %w", beadsDir, err)
} Prevention
- Keep sync.lock owned by the running user with 0600 permissions
- Check ulimit -n when running long-lived processes that open many files
- Verify the filesystem is writable before starting sync operations
- Remove stale lock files only after confirming no live sync process holds them
When it happens
Trigger: os.OpenFile on the lock path fails because the beads directory is not writable, the lock file exists with restrictive ownership/permissions, the path is on a read-only filesystem, or too many file descriptors are open.
Common situations: Another user previously created sync.lock with different ownership; running in a container with a read-only .beads mount; ulimit -n exhausted after leaked file handles; antivirus/backup software locking the file on some platforms.
Related errors
- creating beads directory: %w
- util: creating lock directory: %w
- util: opening lock file: %w
- dolt path is not executable
- failed to create backup directory: %w
AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30).
Data as JSON: /api/errors/cec0dca269389132.
Report an issue: GitHub.