{"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/533346cc374382b41ec5ff70536252b2e96e725c/src/system/history/sync/directories.rs#L85-L121","documentation":"After planning directory permission steps but before applying them, Step::validate re-observes the directory (dev, ino, mode) and requires it to equal the `before` snapshot captured during planning. If anything about the directory changed in between — replaced, recreated, chmod'ed — mise aborts with this retryable error rather than writing permissions derived from a stale snapshot. The plan was computed against state that no longer exists.","triggerScenarios":"`Step::validate` (called from `apply`) observes a (dev, ino, mode) triple that differs from step.before — the directory was deleted and recreated (new inode), chmod'ed, or swapped with another directory between plan() and apply().","commonSituations":"A dotfiles manager or install script recreated the directory while the pull was running; two syncs raced; a chmod ran concurrently with the pull.","solutions":["Re-run `mise dot sync` as the message advises — a fresh plan is computed from current state.","Ensure no other process (package manager, dotfile installer, second sync) recreates or chmods the directory during a pull.","After a recreate, verify the directory's inode/permissions are what you intend before re-syncing."],"exampleFix":"// before\n$ mkdir -p ~/.config/foo && mise dot sync &\n$ rm -rf ~/.config/foo && mkdir ~/.config/foo   # invalidates plan\n// after\n$ mise dot sync   # retry after any directory recreation completes","handlingStrategy":"retry","validationCode":"# capture before/after inode+mode to detect mid-sync changes\nbefore=$(stat -c '%d:%i:%a' ~/.config/foo); mise dot sync; after=$(stat -c '%d:%i:%a' ~/.config/foo); [ \"$before\" = \"$after\" ] || echo 'directory changed during sync — retry'","typeGuard":null,"tryCatchPattern":"for i in 1 2 3; do mise dot sync && break; sleep 2; done","preventionTips":["Don't recreate or chmod synced directories while a pull runs.","Finish installers/declarative dotfile managers before syncing.","Run syncs when no watchers are mutating the tree."],"tags":["concurrency","permissions","sync","retry"],"backgroundTag":"invalid-state-transition","analyzedSha":"533346cc374382b41ec5ff70536252b2e96e725c","analyzedAt":"2026-09-17T13:35:38.149Z","contentChangedAt":"2026-09-17T13:35:38.149Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}