{"record":{"id":"8e1d5637aae2609e","repo":"PrefectHQ/fastmcp","slug":"cli-state-must-not-be-a-symbolic-link-path-name","errorCode":null,"errorMessage":"CLI state must not be a symbolic link: {path.name}","messagePattern":"CLI state must not be a symbolic link: (.+?)","errorType":"exception","errorClass":"StateFileError","httpStatus":null,"severity":"error","filePath":"fastmcp_slim/fastmcp/cli/deploy/state.py","lineNumber":155,"sourceCode":"            import fcntl\n\n            with suppress(OSError):\n                fcntl.flock(lock_file.fileno(), fcntl.LOCK_UN)\n        with suppress(OSError):\n            lock_file.close()\n\n\ndef read_state(\n    path: Path,\n    model: type[ModelT],\n    *,\n    secret: bool = False,\n) -> ModelT | None:\n    \"\"\"Read and validate a versioned JSON state file.\"\"\"\n    if not path.exists():\n        return None\n    if path.is_symlink():\n        raise StateFileError(f\"CLI state must not be a symbolic link: {path.name}\")\n\n    if secret:\n        _restrict_access(path.parent, directory=True)\n        _restrict_access(path)\n\n    try:\n        return model.model_validate_json(path.read_text(encoding=\"utf-8\"))\n    except (ValidationError, ValueError):\n        raise StateFileError(f\"CLI state is invalid: {path.name}\") from None\n    except OSError as exc:\n        raise StateFileError(f\"Could not read CLI state: {path.name}\") from exc\n\n\ndef write_state(path: Path, data: dict[str, Any]) -> None:\n    \"\"\"Write JSON through a restricted temporary file and atomic replacement.\"\"\"\n    _prepare_directory(path.parent)\n    payload = (json.dumps(data, indent=2, sort_keys=True) + \"\\n\").encode()\n    descriptor: int | None = None","sourceCodeStart":137,"sourceCodeEnd":173,"githubUrl":"https://github.com/PrefectHQ/fastmcp/blob/1f021142978e0861cd910c8df4e8074bc7cf3978/fastmcp_slim/fastmcp/cli/deploy/state.py#L137-L173","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\n$ ls -l ~/.fastmcp/state/session.json\nsession.json -> /mnt/shared/session.json\n// after\n$ rm ~/.fastmcp/state/session.json\n$ fastmcp login   # regenerates a real, restricted file","handlingStrategy":"validation","validationCode":"from pathlib import Path\n\ndef assert_real_file(path: Path) -> None:\n    if path.is_symlink():\n        raise RuntimeError(f\"{path} is a symlink; replace with a real file\")\n    if path.exists() and not path.is_file():\n        raise RuntimeError(f\"{path} is not a regular file\")","typeGuard":"def is_regular_state_file(path: Path) -> bool:\n    return path.exists() and not path.is_symlink() and path.is_file()","tryCatchPattern":"try:\n    state = load(state_path)\nexcept StateFileError as exc:\n    if \"symbolic link\" in str(exc):\n        state_path.unlink()\n        state = load(state_path)   # regenerates\n    else:\n        raise","preventionTips":["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"],"tags":["security","symlink","filesystem"],"backgroundTag":"symlink-attack-detected","analyzedSha":"1f021142978e0861cd910c8df4e8074bc7cf3978","analyzedAt":"2026-08-29T14:31:16.082Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}