Hmbown/CodeWhale · error

Automation lock must not be a reparse point

Error message

Automation lock must not be a reparse point

What it means

The automation manager guards its lock file against symlink/reparse-point attacks. On Windows, before wrapping the lock file in an fd_lock::RwLock, it checks the FILE_ATTRIBUTE_REPARSE_POINT bit (0x400) in the file's attributes. If set — meaning the 'lock' is actually a symlink, mount point, or junction — it refuses to proceed so automation state cannot be redirected to an attacker-controlled location.

Solutions

  1. Replace the symlink/junction at the automation lock path with a real regular file
  2. Move the real state directory instead of symlinking it (e.g. change the state dir setting to the actual path)
  3. On non-attack scenarios, copy the lock file contents to a new regular file and delete the reparse point

Example fix

// before: config/state/automation.lock -> D:\state\automation.lock (junction)
// after: copy the real file to config/state/automation.lock and remove the junction
Copy-Item D:\state\automation.lock config\state\automation.lock
Remove-Item config\state\automation.lock.link  # or the junction itself
Defensive patterns

Strategy: try-catch

Validate before calling

#[cfg(windows)]
fn lock_path_is_regular(path: &Path) -> std::io::Result<bool> {
    use std::os::windows::fs::MetadataExt;
    Ok(path.metadata()?.file_attributes() & 0x400 == 0)
}

Type guard

fn is_reparse_point(m: &std::fs::Metadata) -> bool { #[cfg(windows)] { use std::os::windows::fs::MetadataExt; m.file_attributes() & 0x400 != 0 } #[cfg(not(windows))] { let _ = m; false } }

Try / catch

match init_automation_manager() {
    Err(e) if e.to_string().contains("reparse point") => eprintln!("lock path is a symlink/junction; use a real directory"),
    Err(e) => return Err(e),
    Ok(m) => m,
}

Prevention

When it happens

Trigger: Opening/initializing the automation manager's lock when the file at the lock path on Windows carries the reparse-point attribute (0x400), e.g. the lock file path is a symlink or junction.

Common situations: Users who symlink their config/state directory (e.g. dotfiles managed via symlinks, or state moved to another drive via a junction) hit this on Windows because the lock file itself is reached through a reparse point.

Understand the failure class

Background: UnsupportedOperationException and "is not supported" errors: when a library deliberately refuses a call — this error's family across 30 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@433685b202 (2026-09-15). Data as JSON: /api/errors/54f74b29e765854e. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/automation_manager.rs:1036

        let file = options
            .open(&path)
            .with_context(|| format!("open {}", path.display()))?;
        let metadata = file.metadata()?;
        if !metadata.is_file() {
            bail!("Automation lock must be a regular file");
        }
        #[cfg(unix)]
        {
            use std::os::unix::fs::MetadataExt as _;
            if metadata.nlink() != 1 {
                bail!("Automation lock must not have hard links");
            }
        }
        #[cfg(windows)]
        {
            use std::os::windows::fs::MetadataExt as _;
            if metadata.file_attributes() & 0x400 != 0 {
                bail!("Automation lock must not be a reparse point");
            }
        }
        Ok(fd_lock::RwLock::new(file))
    }

    fn with_transaction<T>(&self, operation: impl FnOnce() -> Result<T>) -> Result<T> {
        let mut lock = self.open_lock("state.lock")?;
        let _guard = lock.write().context("lock automation state")?;
        operation()
    }

    /// Short read/modify/write transaction shared with scheduler admission.
    /// Returning None leaves an absent record absent; it does not delete one.
    pub(crate) fn edit_automation(
        &self,
        id: &str,
        edit: impl FnOnce(Option<AutomationRecord>) -> Result<Option<AutomationRecord>>,
    ) -> Result<Option<AutomationRecord>> {

View on GitHub (pinned to 433685b202)