charmbracelet/crush · warning
file lock is held by another process
Error message
file lock is held by another process
What it means
lock.ErrContended is returned by lock.TryFile when an advisory file lock is already held by another process. Unlike lock.File, which blocks until the lock is free or the context is cancelled, TryFile is the non-blocking variant: if the lock is contended it fails fast with this sentinel so callers can decide to skip or retry.
Source
Thrown at internal/lock/lock.go:30
// The lock file at path is created if it does not exist. It is never
// unlinked — flock is keyed by inode, not path, and unlinking could
// create a window where two processes lock different inodes at the
// same path.
//
// This is the canonical file-locking helper for Crush. Callers should
// prefer it over rolling their own platform-specific code.
package lock
import (
"context"
"errors"
"fmt"
"os"
)
// ErrContended is returned by TryFile when the lock is already held by
// another process.
var ErrContended = errors.New("file lock is held by another process")
// File acquires an exclusive advisory lock on the file at path, blocking
// until the lock is acquired or ctx is cancelled. It returns a release
// function that drops the lock and closes the underlying file descriptor.
//
// Pass a context with a deadline (e.g. context.WithTimeout) to bound the
// wait. Pass context.Background() to block indefinitely.
func File(ctx context.Context, path string) (func(), error) {
f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0o600)
if err != nil {
return nil, fmt.Errorf("open lock file %q: %w", path, err)
}
release, err := lockFile(ctx, f)
if err != nil {
f.Close()
return nil, err
}View on GitHub (pinned to 7944b8e522)
Solutions
- Exit the other running crush process that holds the data-dir lock, then retry.
- Use lock.File (blocking, context-bounded) instead of TryFile if waiting is acceptable.
- Point each concurrent instance at a different data directory, or verify no orphaned process still holds the lock (lsof on the lock file).
Example fix
// before
release, err := lock.TryFile(path)
if err != nil {
return err // hard fail on contention
}
// after
release, err := lock.TryFile(path)
if errors.Is(err, lock.ErrContended) {
return nil // another instance owns the lock; skip gracefully
}
if err != nil {
return err
} Defensive patterns
Strategy: type-guard
Validate before calling
// Check whether another crush instance is running before acquiring
if _, err := os.Stat(lockPath); err == nil {
if holders := processesHolding(lockPath); len(holders) > 0 { /* another instance alive */ }
} Type guard
func isLockContended(err error) bool { return errors.Is(err, lock.ErrContended) } Try / catch
release, err := lock.TryFile(path)
if errors.Is(err, lock.ErrContended) {
return nil // or retry with backoff / fall back to lock.File
}
if err != nil {
return err
}
defer release() Prevention
- Run only one crush instance per data directory, or isolate data dirs per instance.
- Check for orphaned processes holding the lock (lsof the lock file) after crashes.
- Use errors.Is with ErrContended to branch, and lock.File with a deadline when waiting is acceptable.
When it happens
Trigger: Two crush processes starting simultaneously and both calling acquireDataDirLock/TryFile on the same data-dir lock file; TryFile against a lock held by a still-running previous instance.
Common situations: Launching a second crush instance while one is already running against the same project data dir; leftover processes after a crash still holding the fd; shared home/data directories across containers pointing at the same lock file.
Related errors
- mcp '%s' already has an authentication in progress
- acquire refresh lock for provider %s: %w
- server is hosting live workspaces
- unsupported by the running server
- clipboard operations are not supported on this platform
AI-assisted analysis of charmbracelet/crush@7944b8e522 (2026-08-29).
Data as JSON: /api/errors/35e08ba98a5c058c.
Report an issue: GitHub.