tinyhumansai/openhuman · error

workspace lock file {lock_path:?} is a symlink; refusing to

Error message

workspace lock file {lock_path:?} is a symlink; refusing to follow it

What it means

Security guard in the MEMORY.md cross-process lock acquisition: the sentinel .memory-write.lock file in the workspace is a symbolic link. Because flock would follow it, the tool refuses rather than lock an attacker-chosen target — a deliberate TOCTOU mitigation.

Source

Thrown at src/openhuman/tools/impl/filesystem/update_memory_md.rs:71

/// `MEMORY.md` mid read-modify-write. This takes an `fs2` exclusive `flock` on a
/// sentinel `.memory-write.lock` file in the workspace; the returned `File`
/// holds the lock until it is dropped (end of the write). `flock` acquisition
/// blocks, so it runs on a blocking thread.
async fn acquire_cross_process_write_lock(workspace_dir: &Path) -> anyhow::Result<std::fs::File> {
    let lock_path = workspace_dir.join(".memory-write.lock");
    tokio::task::spawn_blocking(move || {
        use fs2::FileExt;
        if let Some(parent) = lock_path.parent() {
            let _ = std::fs::create_dir_all(parent);
        }
        // Reject a symlinked lock file: a project-controlled `.memory-write.lock`
        // symlink (including a dangling one) could otherwise redirect this
        // create/open/lock to a path OUTSIDE the already-containment-checked
        // workspace, bypassing the symlink hardening applied to MEMORY.md /
        // SKILL.md. If it exists it must be a regular file.
        if let Ok(meta) = std::fs::symlink_metadata(&lock_path) {
            if meta.file_type().is_symlink() {
                return Err(anyhow::anyhow!(
                    "workspace lock file {lock_path:?} is a symlink; refusing to follow it"
                ));
            }
        }
        let mut opts = std::fs::OpenOptions::new();
        opts.create(true).write(true).truncate(false);
        #[cfg(unix)]
        {
            // O_NOFOLLOW closes the TOCTOU window: if a symlink is swapped in
            // after the check above, the open fails (ELOOP) rather than follows.
            use std::os::unix::fs::OpenOptionsExt;
            opts.custom_flags(libc::O_NOFOLLOW);
        }
        let file = opts
            .open(&lock_path)
            .map_err(|e| anyhow::anyhow!("open workspace lock file {lock_path:?}: {e}"))?;
        file.lock_exclusive()
            .map_err(|e| anyhow::anyhow!("acquire workspace write flock: {e}"))?;

View on GitHub (pinned to 7491200858)

Solutions

  1. Delete the symlinked .memory-write.lock from the workspace
  2. Investigate how the symlink was created — it may indicate tampering
  3. The next write attempt will recreate a regular lock file
Defensive patterns

Strategy: validation

When it happens

Trigger: Thrown at src/openhuman/tools/impl/filesystem/update_memory_md.rs:71 when the library encounters an invalid state.

Common situations: See trigger scenarios.


AI-assisted analysis of tinyhumansai/openhuman@7491200858 (2026-08-17). Data as JSON: /api/errors/915e1d937a34e31d. Report an issue: GitHub.