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

  1. Fix ownership/permissions on <beadsDir> and sync.lock so the running user can create/read-write (0600)
  2. Check file descriptor usage (ulimit -n, lsof) for leaked handles if the error is EMFILE
  3. Verify the filesystem is writable and not read-only mounted
  4. 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

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


AI-assisted analysis of gastownhall/beads@71377f2769 (2026-08-30). Data as JSON: /api/errors/cec0dca269389132. Report an issue: GitHub.