PrefectHQ/fastmcp · error · StateFileError

The CLI state lock must not be a symbolic link

Error message

The CLI state lock must not be a symbolic link

What it means

StateFileError raised by state_lock when the lock file .state.lock inside the state directory is a symbolic link. A symlinked lock could redirect writes outside the state directory (a tampering/TOCTOU risk), so the CLI refuses to take the lock.

Source

Thrown at fastmcp_slim/fastmcp/cli/deploy/state.py:101

    except OSError as exc:
        raise StateFileError("Could not restrict access to CLI state") from exc


def _prepare_directory(path: Path) -> None:
    try:
        path.mkdir(parents=True, exist_ok=True)
    except OSError as exc:
        raise StateFileError("Could not create the CLI state directory") from exc
    _restrict_access(path, directory=True)


@contextmanager
def state_lock(directory: Path) -> Iterator[None]:
    """Lock related CLI state changes across processes."""
    _prepare_directory(directory)
    lock_path = directory / ".state.lock"
    if lock_path.is_symlink():
        raise StateFileError("The CLI state lock must not be a symbolic link")

    lock_file = None
    try:
        lock_file = lock_path.open("a+b")
        _restrict_access(lock_path)
        if os.name == "nt":
            import msvcrt

            if lock_path.stat().st_size == 0:
                lock_file.write(b"\0")
                lock_file.flush()
            lock_file.seek(0)
            msvcrt.locking(lock_file.fileno(), msvcrt.LK_LOCK, 1)
        else:
            import fcntl

            fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX)
    except (OSError, StateFileError) as exc:

View on GitHub (pinned to 1f02114297)

Solutions

  1. Inspect and remove the link: ls -l <state-dir>/.state.lock then rm <state-dir>/.state.lock
  2. Ensure the state directory is only writable by your user (0o700)
  3. Exclude the state directory from sync/backup tools that may recreate symlinks
  4. Retry the CLI command after removing the link

Example fix

// before
$ ls -l ~/.fastmcp/state/.state.lock
lrwxr-xr-x .state.lock -> /tmp/evil
// after
$ rm ~/.fastmcp/state/.state.lock
$ chmod 700 ~/.fastmcp/state
Defensive patterns

Strategy: validation

Validate before calling

from pathlib import Path

lock_path = state_dir / ".state.lock"
if lock_path.is_symlink():
    raise RuntimeError(
        f"{lock_path} is a symlink; remove it and secure the directory"
    )

Type guard

def is_safe_lockfile(path: Path) -> bool:
    return path.exists() is False or not path.is_symlink()

Try / catch

try:
    with state_lock(state_dir):
        ...
except StateFileError:
    (state_dir / ".state.lock").unlink(missing_ok=True)
    with state_lock(state_dir):
        ...

Prevention

When it happens

Trigger: Entering state_lock (via _load_session_snapshot, set_api_origin, save_for_origin, clear_if_matches) after something replaced directory/.state.lock with a symlink — attacker tampering, a leftover link from a broken sync/backup tool, or a malicious shared-directory setup.

Common situations: State directory synced by Dropbox/OneDrive/git that restored a symlink; multi-user /tmp-style shared directory where another account planted the link; a previous crash or third-party script created the link.

Related errors


AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29). Data as JSON: /api/errors/9009a15959607e71. Report an issue: GitHub.