gitbutlerapp/gitbutler · warning · anyhow::Error
Project at '{}' is already opened for writing by another Git
Error message
Project at '{}' is already opened for writing by another GitButler instance What it means
`try_exclusive_inter_process_access(project_data, LockScope::AllOperations)` takes an advisory lock file (`project.lock`) in the project's data directory. If `try_lock()` reports it's already held, another live GitButler process owns the project for writing, and this bail fires. The lock is released on process exit for any reason, so it cannot go stale — the competing process is genuinely alive.
Source
Thrown at crates/but-core/src/sync.rs:68
let got_lock = lock
.try_lock()
.context("Failed to check if lock is taken")?;
if !got_lock {
let error_message = match scope {
LockScope::AllOperations => {
format!(
"Project at '{}' is already opened for writing by another GitButler instance",
project_data.display()
)
}
LockScope::BackgroundRefreshOperations => {
format!(
"Project at '{}' is already being refreshed in the background by another GitButler instance",
project_data.display()
)
}
};
bail!(error_message);
}
Ok(lock)
}
/// Return a guard for exclusive (read+write) *in-process* repository access for the project at
/// `git_dir`, blocking while waiting for someone else in this process to release it, or for all
/// readers to disappear. Locking is fair.
/// Also use `project_data_dir` if `Some` to create an *inter-process* exclusive lock.
/// Creating, opening, or locking that file is best-effort. Failures are logged and ignored, so
/// the hard guarantee provided by this function remains in-process exclusivity only.
///
/// If the current process inherits Git's commit-hook environment (`GIT_EDITOR=:` together with
/// `GIT_INDEX_FILE`), acquiring the inter-process lock becomes non-blocking: if another process
/// already holds it, we continue without that file lock instead of waiting. This avoids
/// deadlocking hook re-entry when the parent GitButler command is already holding the same
/// inter-process lock while waiting for the hook to finish.
/// If `project_data_dir` is `None`, no inter-process lock is obtained.
///View on GitHub (pinned to caf1f223d3)
Solutions
- Close the other GitButler instance (quit the desktop app fully, including tray) and retry.
- Find lingering holders: `ps aux | grep -i gitbutler` (or check the process holding the lock with `lsof <project_data>/project.lock`) and exit it normally.
- If the holder is a stuck process, kill it — the OS then releases the advisory lock since it cannot go stale on its own.
- For CLI tools meant to run alongside the app, use scopes/APIs designed for coexistence instead of the full AllOperations lock.
Defensive patterns
Strategy: retry
Validate before calling
// Check whether the lock is currently held before attempting a full open
use std::fs::File;
use fs4::FileExt; // same family but-core uses for the advisory lock
fn project_lock_free(project_data: &std::path::Path) -> bool {
File::open(project_data.join("project.lock"))
.and_then(|f| f.try_lock_exclusive().map(|_| true))
.unwrap_or(false)
} Try / catch
// Treat as contention: prompt to close the other instance, then retry
for attempt in 0..3 {
match try_exclusive_inter_process_access(&dir, LockScope::AllOperations) {
Ok(lock) => return Ok(lock),
Err(err) if err.to_string().contains("another GitButler instance") && attempt < 2 => {
notify_user("Close the other GitButler window/app, then retrying…");
std::thread::sleep(std::time::Duration::from_secs(2));
}
Err(err) => return Err(err),
}
} Prevention
- Run exactly one GitButler instance per project data directory; quit the desktop app (tray included) before heavy CLI work.
- Since the lock auto-releases on process death, never delete lock files manually — find the live holder instead (`lsof <dir>/project.lock`).
- In automation, retry with backoff rather than failing on first contention.
When it happens
Trigger: Opening the same project in a second GitButler desktop instance, or running a GitButler CLI/TUI command against a project the desktop app currently holds; two `but` invocations racing on the same project data directory.
Common situations: Desktop app running in the tray while the user runs `but` in a terminal; a second app window/installation pointed at the same project directory; a hung previous instance that is still alive but invisible.
Related errors
- Project at '{}' is already being refreshed in the background
- diffing-thread crashed
- Failed to communicate with LM Studio server: ${error instanc
- Invalid response: ${JSON.stringify(result)}
- When using GitButler's API to summarize code, you must be lo
AI-assisted analysis of gitbutlerapp/gitbutler@caf1f223d3 (2026-08-20).
Data as JSON: /api/errors/5b5366729660fafc.
Report an issue: GitHub.