{"record":{"id":"581f0c5818cd90a0","repo":"gitbutlerapp/gitbutler","slug":"another-pre-commit-hook-is-already-using-the-repos","errorCode":null,"errorMessage":"another pre-commit hook is already using the repository index","messagePattern":"another pre-commit hook is already using the repository index","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"crates/gitbutler-repo/src/hooks.rs","lineNumber":105,"sourceCode":"    // Back up the index file byte for byte; a round-trip through a tree would fail\n    // on an index with unmerged entries (a conflict in an uncommitted file) and\n    // could not bring those entries back. A sibling file copy keeps memory flat and\n    // lets the restore be a single atomic rename that also keeps the permissions.\n    let index_path = repo\n        .index()?\n        .path()\n        .context(\"repository index has no backing file\")?\n        .to_owned();\n    let backup_path = index_path.with_extension(\"gitbutler-hook-backup\");\n    let backup_tmp_path = index_path.with_extension(\"gitbutler-hook-backup.tmp\");\n    let mut transaction_lock =\n        but_core::sync::LockFile::open(index_path.with_extension(\"gitbutler-hook-lock\"))\n            .context(\"failed to open pre-commit index lock\")?;\n    if !transaction_lock\n        .try_lock()\n        .context(\"failed to lock the index for a pre-commit hook\")?\n    {\n        anyhow::bail!(\"another pre-commit hook is already using the repository index\");\n    }\n    match std::fs::symlink_metadata(&backup_path) {\n        Ok(_) => anyhow::bail!(\n            \"stale pre-commit index backup at '{}'; restore it to '{}' before retrying\",\n            backup_path.display(),\n            index_path.display()\n        ),\n        Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}\n        Err(err) => return Err(err).context(\"failed to inspect pre-commit index backup\"),\n    }\n    match std::fs::remove_file(&backup_tmp_path) {\n        Ok(()) => {}\n        Err(err) if err.kind() == std::io::ErrorKind::NotFound => {}\n        Err(err) => return Err(err).context(\"failed to remove stale temporary index backup\"),\n    }\n    let had_index = match std::fs::copy(&index_path, &backup_tmp_path) {\n        Ok(_) => {\n            std::fs::rename(&backup_tmp_path, &backup_path)","sourceCodeStart":87,"sourceCodeEnd":123,"githubUrl":"https://github.com/gitbutlerapp/gitbutler/blob/caf1f223d3cfb94488c9198ad34487c6006c648f/crates/gitbutler-repo/src/hooks.rs#L87-L123","documentation":"Thrown by pre_commit_with_tree when the non-blocking lock file .git/index.gitbutler-hook-lock is already held. The hook flow swaps the git index for a temporary tree to run pre-commit hooks, so it takes an exclusive try-lock first; refusal means another commit/hook transaction is in flight right now. No state was changed when this error is returned, so a retry is safe.","triggerScenarios":"Two commit paths entering pre_commit_with_tree for the same repository concurrently - e.g. the GitButler UI committing while the 'but' CLI or an agent commits, or parallel jobs in one worktree - so LockFile::try_lock returns false.","commonSituations":"AI agents committing via the CLI while the desktop app auto-commits; scripts racing the app's scheduled commits; a leftover lock file from a killed process with no live holder blocking all later attempts.","solutions":["Retry the commit once the in-flight operation finishes - the lock is released when the other hook transaction completes","Serialize commit operations per repository: one app instance or one CLI invocation at a time","If no other GitButler process is running, check for a stale .git/index.gitbutler-hook-lock left by a crashed process and remove it"],"exampleFix":"// before: concurrent commits -> \"another pre-commit hook is already using the repository index\"\n// after: retry with backoff until the lock is free\nlet mut attempt = 0;\nloop {\n    match run_pre_commit(ctx, tree_id) {\n        Err(e) if e.to_string().contains(\"another pre-commit hook\") && attempt < 5 => {\n            attempt += 1;\n            std::thread::sleep(std::time::Duration::from_millis(200 * attempt));\n        }\n        r => break r,\n    }\n}","handlingStrategy":"retry","validationCode":"let lock = index_path.with_extension(\"gitbutler-hook-lock\");\nif lock.exists() {\n    // another hook transaction may be in flight - wait or serialize before committing\n}","typeGuard":null,"tryCatchPattern":"match pre_commit_with_tree(ctx, tree_id) {\n    Err(e) if e.to_string().contains(\"another pre-commit hook\") => {\n        // transient contention: wait for the other commit to finish, then retry\n    }\n    other => other,\n}","preventionTips":["Run a single writer (app or CLI) per repository","Queue commits instead of firing them in parallel","Clean up lock files after killing processes mid-commit"],"tags":["git","pre-commit","concurrency","file-lock","transient"],"backgroundTag":"file-lock-contention","analyzedSha":"caf1f223d3cfb94488c9198ad34487c6006c648f","analyzedAt":"2026-08-20T07:55:40.983Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}