Hmbown/CodeWhale · error

Refusing a symlinked settings lock at

Error message

Refusing a symlinked settings lock at {}

What it means

Before locking, the code checks the lock path with symlink_metadata and refuses to proceed if it is a symlink. A planted `settings.toml.lock -> /some/target` would let an attacker choose which file the app creates with its own permissions, so locking through symlinks is rejected outright. This is a deliberate hardening measure, not a transient failure.

Solutions

  1. Replace the symlink with a real regular file: `rm <settings.toml.lock> && touch <settings.toml.lock>`
  2. Reconfigure your dotfile manager to copy rather than symlink this specific file
  3. Retry the settings operation to recreate a fresh regular lock file

Example fix

// before (shell)
ls -la settings.toml.lock  # settings.toml.lock -> /etc/passwd
// after (shell)
rm settings.toml.lock && touch settings.toml.lock
Defensive patterns

Strategy: validation

Validate before calling

fn lock_is_symlink(p: &std::path::Path) -> bool {
    std::fs::symlink_metadata(p).map(|m| m.file_type().is_symlink()).unwrap_or(false)
}
// if true, replace with a real file before calling the library

Try / catch

if lock_is_symlink(&lock_path) {
    std::fs::remove_file(&lock_path).ok();
    std::fs::File::create(&lock_path).ok();
}
let lock = acquire_settings_lock()?;

Prevention

When it happens

Trigger: Any settings lock acquisition when `settings.toml.lock` is a symlink (e.g. pointing elsewhere on disk), detected via std::fs::symlink_metadata in reject_settings_lock_symlink.

Common situations: Dotfile managers that symlink config directories wholesale (stow, homesick, dotbot); an attacker planting a symlink to escalate file creation; sync tools that replaced the lock with a link.

Understand the failure class

Background: Permission denied / not authorized / 403 Forbidden: access-control rejections when the caller lacks the required role, grant, or ownership — this error's family across 18 libraries.

Related errors


AI-assisted analysis of Hmbown/CodeWhale@73e0f67d83 (2026-09-22). Data as JSON: /api/errors/b671f7b1f9d81766. Report an issue: GitHub.

Appendix: source

Thrown at crates/tui/src/settings.rs:2375

    {
        anyhow::bail!(
            "Refusing a non-regular settings lock at {}",
            lock_path.display()
        );
    }

    let mut lock = fd_lock::RwLock::new(lock_file);
    let _guard = lock
        .write()
        .with_context(|| format!("Failed to acquire settings lock at {}", lock_path.display()))?;
    operation()
}

/// Refuse to lock through a symlink: a planted `settings.toml.lock -> …` would
/// otherwise let an attacker pick which file we create with our permissions.
fn reject_settings_lock_symlink(lock_path: &Path) -> Result<()> {
    match std::fs::symlink_metadata(lock_path) {
        Ok(metadata) if metadata.file_type().is_symlink() => anyhow::bail!(
            "Refusing a symlinked settings lock at {}",
            lock_path.display()
        ),
        Ok(_) | Err(_) => Ok(()),
    }
}

/// Replace `path` with `body` by writing an adjacent temporary file and
/// renaming it into place.
///
/// A direct `fs::write` truncates first, so any concurrent reader — another
/// Codewhale process, an editor, a `cat` — can observe a half-written file and
/// parse it as truncated TOML, silently losing every key past the tear. A
/// same-directory temp file plus the platform's replace primitive makes the
/// swap atomic for readers: they see either the whole previous file or the
/// whole new one.
///
/// The temp file inherits the existing file's permission bits when there is one

View on GitHub (pinned to 73e0f67d83)