{"record":{"id":"49f09f393e46b82b","repo":"jdx/mise","slug":"changed-while-preparing-enrollment-concurrent","errorCode":null,"errorMessage":"{} changed while preparing enrollment; concurrent declaration edit preserved; retry","messagePattern":"(.+?) changed while preparing enrollment; concurrent declaration edit preserved; retry","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/cli/dotfiles/track.rs","lineNumber":316,"sourceCode":"    declaration_lock_for(&crate::config::global_shared_config_path())\n}\n\nfn declaration_lock_for(config: &Path) -> Result<fslock::LockFile> {\n    crate::lock_file::LockFile::new(&config.with_extension(\"dotfiles-declarations.lock\"))\n        .try_lock()?\n        .ok_or_else(|| {\n            eyre::eyre!(\"another tracking declaration command is running; retry shortly\")\n        })\n}\n\nfn check_declaration(path: &Path, expected: Option<&str>) -> Result<()> {\n    let current = match std::fs::read_to_string(path) {\n        Ok(body) => Some(body),\n        Err(error) if error.kind() == std::io::ErrorKind::NotFound => None,\n        Err(error) => return Err(error.into()),\n    };\n    if current.as_deref() != expected {\n        bail!(\n            \"{} changed while preparing enrollment; concurrent declaration edit preserved; retry\",\n            display_path(path)\n        );\n    }\n    Ok(())\n}\n\nfn commit_declaration(\n    path: &Path,\n    expected: Option<&str>,\n    prepared: file::PreparedAtomicWrite,\n) -> Result<()> {\n    // Check after formatting, directory creation, writing, and fsync. The\n    // declaration lock coordinates mise writers; unrelated editors do not\n    // participate, so this is not a filesystem compare-and-swap guarantee.\n    check_declaration(path, expected)?;\n    prepared.commit()\n}","sourceCodeStart":298,"sourceCodeEnd":334,"githubUrl":"https://github.com/jdx/mise/blob/afd2eddd3a50c16190efc1c7e94404b48f72af57/src/cli/dotfiles/track.rs#L298-L334","documentation":"While enrolling a dotfile, mise verifies the declaration file's contents still match what was read earlier (`expected`). If the on-disk content changed between read and commit — a concurrent edit — track aborts instead of overwriting someone else's change, telling the user to retry.","triggerScenarios":"`dotfiles track` preparing enrollment of a declaration (e.g. declarations file in mise config) while another process/editor modifies that file between the initial read and the `check_declaration` compare-then-commit.","commonSituations":"Two terminals running `dotfiles track`/config edits simultaneously; an editor auto-saving the declaration file mid-command; sync tools (dropbox/git hooks) touching the file during enrollment.","solutions":["Retry `mise bootstrap dotfiles track <target>` once the other edit settles","Ensure no editor or process is writing the declaration file concurrently","Commit declaration edits through one tool at a time"],"exampleFix":null,"handlingStrategy":"retry","validationCode":"# ensure no writer holds the declaration file before tracking\nflock -n /path/to/declarations.lock -c 'mise bootstrap dotfiles track ~/target' || echo 'declaration busy'","typeGuard":null,"tryCatchPattern":"until mise bootstrap dotfiles track ~/target 2>&1; do\n  case \"$?\" in 0) break;; esac\n  grep -q 'concurrent declaration edit' log && sleep 1 || break\ndone","preventionTips":["Close editors holding the declaration file open with autosave","Run one dotfiles track/config-edit process at a time","Pause file-sync tools (Dropbox/syncthing) during bulk enrollment"],"tags":["dotfiles","concurrency","race-condition"],"backgroundTag":"conflicting-config-options","analyzedSha":"afd2eddd3a50c16190efc1c7e94404b48f72af57","analyzedAt":"2026-09-09T01:38:25.179Z","contentChangedAt":"2026-09-09T01:38:25.179Z","schemaVersion":2},"datasetVersion":"2026-09-14T00:17:10.932Z"}