{"record":{"id":"6eb8602a9a4b0d51","repo":"jdx/mise","slug":"changed-after-permission-planning-retry-pull","errorCode":null,"errorMessage":"{} changed after permission planning; retry pull","messagePattern":"(.+?) changed after permission planning; retry pull","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/system/history/sync/directories.rs","lineNumber":103,"sourceCode":"            );\n        }\n        if before.map(|(_, _, bits)| bits) != Some(desired) {\n            steps.push(Step {\n                path,\n                before,\n                desired,\n                written: None,\n            });\n        }\n    }\n    steps.sort_by_key(|step| step.path.components().count());\n    Ok(steps)\n}\n\nimpl Step {\n    pub(super) fn validate(&self) -> Result<()> {\n        if observe(&self.path)? != self.before {\n            bail!(\n                \"{} changed after permission planning; retry pull\",\n                crate::file::display_path(&self.path)\n            );\n        }\n        Ok(())\n    }\n\n    pub(super) fn apply(&mut self) -> Result<()> {\n        self.validate()?;\n        let pending = journal::begin_changes_with(\n            \"history\",\n            \"directory permissions\",\n            [(self.path.clone(), journal::Capture::Shallow)],\n        )?;\n        if self.before.is_none() {\n            crate::file::create_dir_all(&self.path)?;\n        }\n        set_mode(&self.path, self.desired)?;","sourceCodeStart":85,"sourceCodeEnd":121,"githubUrl":"https://github.com/jdx/mise/blob/afd2eddd3a50c16190efc1c7e94404b48f72af57/src/system/history/sync/directories.rs#L85-L121","documentation":"validate() re-observes a directory right before applying a planned permission step and compares against the 'before' snapshot taken during planning. Any change (identity or mode) between plan and apply aborts with a retry instruction, preventing permissions from being applied to a swapped directory.","triggerScenarios":"Directory deleted/recreated, replaced by a file/symlink, or chmodded between plan() and validate() inside apply.","commonSituations":"Another process (installer, sync tool) touching the directory mid-pull; two mise pulls racing; user editing permissions while a pull runs.","solutions":["Re-run the pull; it re-plans against the current directory state.","Stop whatever process is mutating the directory concurrently, then retry.","Verify the directory's dev/ino/mode stability (nothing recreating it) before retrying."],"exampleFix":null,"handlingStrategy":"retry","validationCode":"let before = observe(&path)?; // compare immediately before apply\nif before != planned_before { /* re-plan */ }","typeGuard":null,"tryCatchPattern":"if let Err(e) = apply(&plan) {\n    if e.to_string().ends_with(\"changed after permission planning; retry pull\") {\n        // simply retry the pull\n    }\n}","preventionTips":["Don't touch/chmod tracked directories while a pull runs","Run pulls serially, not concurrently","Disable agents that recreate directories mid-operation"],"tags":["filesystem","permissions","race-condition","sync"],"backgroundTag":"invalid-state-transition","analyzedSha":"afd2eddd3a50c16190efc1c7e94404b48f72af57","analyzedAt":"2026-09-09T01:38:25.179Z","contentChangedAt":"2026-09-09T01:38:25.179Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}