{"record":{"id":"9009a15959607e71","repo":"PrefectHQ/fastmcp","slug":"the-cli-state-lock-must-not-be-a-symbolic-link","errorCode":null,"errorMessage":"The CLI state lock must not be a symbolic link","messagePattern":"The CLI state lock must not be a symbolic link","errorType":"exception","errorClass":"StateFileError","httpStatus":null,"severity":"error","filePath":"fastmcp_slim/fastmcp/cli/deploy/state.py","lineNumber":101,"sourceCode":"    except OSError as exc:\n        raise StateFileError(\"Could not restrict access to CLI state\") from exc\n\n\ndef _prepare_directory(path: Path) -> None:\n    try:\n        path.mkdir(parents=True, exist_ok=True)\n    except OSError as exc:\n        raise StateFileError(\"Could not create the CLI state directory\") from exc\n    _restrict_access(path, directory=True)\n\n\n@contextmanager\ndef state_lock(directory: Path) -> Iterator[None]:\n    \"\"\"Lock related CLI state changes across processes.\"\"\"\n    _prepare_directory(directory)\n    lock_path = directory / \".state.lock\"\n    if lock_path.is_symlink():\n        raise StateFileError(\"The CLI state lock must not be a symbolic link\")\n\n    lock_file = None\n    try:\n        lock_file = lock_path.open(\"a+b\")\n        _restrict_access(lock_path)\n        if os.name == \"nt\":\n            import msvcrt\n\n            if lock_path.stat().st_size == 0:\n                lock_file.write(b\"\\0\")\n                lock_file.flush()\n            lock_file.seek(0)\n            msvcrt.locking(lock_file.fileno(), msvcrt.LK_LOCK, 1)\n        else:\n            import fcntl\n\n            fcntl.flock(lock_file.fileno(), fcntl.LOCK_EX)\n    except (OSError, StateFileError) as exc:","sourceCodeStart":83,"sourceCodeEnd":119,"githubUrl":"https://github.com/PrefectHQ/fastmcp/blob/1f021142978e0861cd910c8df4e8074bc7cf3978/fastmcp_slim/fastmcp/cli/deploy/state.py#L83-L119","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Inspect and remove the link: ls -l <state-dir>/.state.lock then rm <state-dir>/.state.lock","Ensure the state directory is only writable by your user (0o700)","Exclude the state directory from sync/backup tools that may recreate symlinks","Retry the CLI command after removing the link"],"exampleFix":"// before\n$ ls -l ~/.fastmcp/state/.state.lock\nlrwxr-xr-x .state.lock -> /tmp/evil\n// after\n$ rm ~/.fastmcp/state/.state.lock\n$ chmod 700 ~/.fastmcp/state","handlingStrategy":"validation","validationCode":"from pathlib import Path\n\nlock_path = state_dir / \".state.lock\"\nif lock_path.is_symlink():\n    raise RuntimeError(\n        f\"{lock_path} is a symlink; remove it and secure the directory\"\n    )","typeGuard":"def is_safe_lockfile(path: Path) -> bool:\n    return path.exists() is False or not path.is_symlink()","tryCatchPattern":"try:\n    with state_lock(state_dir):\n        ...\nexcept StateFileError:\n    (state_dir / \".state.lock\").unlink(missing_ok=True)\n    with state_lock(state_dir):\n        ...","preventionTips":["chmod 700 the state directory so other users cannot plant links","Exclude the state directory from git/sync tools that follow or create symlinks","Periodically audit the state dir: find ~/.fastmcp -type l"],"tags":["security","symlink","filesystem","lockfile"],"backgroundTag":"symlink-attack-detected","analyzedSha":"1f021142978e0861cd910c8df4e8074bc7cf3978","analyzedAt":"2026-08-29T14:31:16.082Z","schemaVersion":2},"datasetVersion":"2026-08-29T17:17:51.833Z"}