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
- Replace the symlink/junction at the automation lock path with a real regular file
- Move the real state directory instead of symlinking it (e.g. change the state dir setting to the actual path)
- 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
- Don't symlink or junction the app's state/config directory on Windows
- Use the app's own settings to relocate state instead of filesystem redirection
- Check with `fsutil reparsepoint query` if you suspect redirection
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
- external credential path must name a non-reparse regular…
- is a reparse point, not a workspace-owned file
- plugin directory is a reparse point or non-directory
- plugin identity probe found a reparse point, hard link, or…
- skill state lock at must be a regular, non-reparse file
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)