PrefectHQ/fastmcp · error · StateFileError
CLI state must not be a symbolic link: {path.name}
Error message
CLI state must not be a symbolic link: {path.name} What it means
StateFileError raised by read_state when an existing state file is a symbolic link. Like the lock-file check, this blocks redirecting reads (and secret re-permissioning) to arbitrary paths — a tamper vector — so the CLI rejects symlinked state outright.
Source
Thrown at fastmcp_slim/fastmcp/cli/deploy/state.py:155
import fcntl
with suppress(OSError):
fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN)
with suppress(OSError):
lock_file.close()
def read_state(
path: Path,
model: type[ModelT],
*,
secret: bool = False,
) -> ModelT | None:
"""Read and validate a versioned JSON state file."""
if not path.exists():
return None
if path.is_symlink():
raise StateFileError(f"CLI state must not be a symbolic link: {path.name}")
if secret:
_restrict_access(path.parent, directory=True)
_restrict_access(path)
try:
return model.model_validate_json(path.read_text(encoding="utf-8"))
except (ValidationError, ValueError):
raise StateFileError(f"CLI state is invalid: {path.name}") from None
except OSError as exc:
raise StateFileError(f"Could not read CLI state: {path.name}") from exc
def write_state(path: Path, data: dict[str, Any]) -> None:
"""Write JSON through a restricted temporary file and atomic replacement."""
_prepare_directory(path.parent)
payload = (json.dumps(data, indent=2, sort_keys=True) + "\n").encode()
descriptor: int | None = NoneView on GitHub (pinned to 1f02114297)
Solutions
- Remove the symlink and replace it with a real file (rm the link, then re-run the CLI to regenerate state)
- Ensure the state directory is user-only writable (0o700) to prevent tampering
- Exclude the state path from dotfile/sync tooling that creates symlinks
Example fix
// before $ ls -l ~/.fastmcp/state/session.json session.json -> /mnt/shared/session.json // after $ rm ~/.fastmcp/state/session.json $ fastmcp login # regenerates a real, restricted file
Defensive patterns
Strategy: validation
Validate before calling
from pathlib import Path
def assert_real_file(path: Path) -> None:
if path.is_symlink():
raise RuntimeError(f"{path} is a symlink; replace with a real file")
if path.exists() and not path.is_file():
raise RuntimeError(f"{path} is not a regular file") Type guard
def is_regular_state_file(path: Path) -> bool:
return path.exists() and not path.is_symlink() and path.is_file() Try / catch
try:
state = load(state_path)
except StateFileError as exc:
if "symbolic link" in str(exc):
state_path.unlink()
state = load(state_path) # regenerates
else:
raise Prevention
- Do not symlink state files via dotfile managers; copy real files instead
- Keep the state directory user-only (0o700)
- Audit for links: find ~/.fastmcp -type l
- Exclude the state path from Dropbox/OneDrive restore behavior
When it happens
Trigger: Calling read_state (via load) on a state path where path.exists() is true but path.is_symlink() is also true — e.g. the auth/session state file was replaced by a symlink by a sync tool or attacker.
Common situations: Dotfile managers or git repos symlinking state files; Dropbox/OneDrive restoring a link; shared multi-user directory tampering; a user manually linking state between machines.
Related errors
- The CLI state lock must not be a symbolic link
- Could not restrict access to CLI state
- Could not create the CLI state directory
- Could not lock CLI state
- Could not read CLI state: {path.name}
AI-assisted analysis of PrefectHQ/fastmcp@1f02114297 (2026-08-29).
Data as JSON: /api/errors/8e1d5637aae2609e.
Report an issue: GitHub.