Automating CS2 Game Data with Selenium, Multi-VM Architecture & Cookie Management
The Client & The Problem
A client named Karol came to me with a tricky automation project. He needed to:
- Scrape XP and level data from the Steam web application
- Auto-launch CS2 and navigate to the main menu
- Open CS2 into Deathmatch mode automatically
- Scrape data from the Steam web app alongside the game client
The challenge wasn't just the automation — it was managing Steam session cookies, coordinating across 5 different machines, and keeping everything secure.
Why Selenium Over Playwright
I chose Selenium over Playwright for this project. The main reason: security. Steam's authentication is sensitive — login credentials, session cookies, and game access all needed to be handled carefully. Selenium's more mature ecosystem gave me better control over the browser profile, cookie storage, and session persistence.
from selenium import webdriver
from selenium.webdriver.chrome.options import Options
from selenium.webdriver.common.by import By
import pickle
import os
COOKIE_FILE = "steam_cookies.pkl"
def create_driver():
opts = Options()
opts.add_argument("--disable-blink-features=AutomationControlled")
opts.add_argument("--no-sandbox")
opts.add_argument("--disable-dev-shm-usage")
opts.add_experimental_option("excludeSwitches", ["enable-automation"])
opts.add_experimental_option("useAutomationExtension", False)
return webdriver.Chrome(options=opts)
The Cookie Challenge
The biggest headache? Steam cookies take a long time to expire — but when they do, you can't do anything until you re-authenticate.
Here's how I handled it:
def load_or_login(driver, username, password):
if os.path.exists(COOKIE_FILE):
driver.get("https://steamcommunity.com")
with open(COOKIE_FILE, "rb") as f:
cookies = pickle.load(f)
for cookie in cookies:
driver.add_cookie(cookie)
driver.refresh()
# Check if login actually stuck
if "login" in driver.current_url:
return full_login(driver, username, password)
return True # Cookies were valid
else:
return full_login(driver, username, password)
def full_login(driver, username, password):
driver.get("https://steamcommunity.com/login/home")
# Fill credentials
driver.find_element(By.ID, "steamAccountName").send_keys(username)
driver.find_element(By.ID, "steamPassword").send_keys(password)
driver.find_element(By.ID, "loginBtn").click()
# Wait for redirect
WebDriverWait(driver, 30).until(
EC.url_changes("https://steamcommunity.com/login/home")
)
# Save cookies for next time
with open(COOKIE_FILE, "wb") as f:
pickle.dump(driver.get_cookies(), f)
return True
The system checks cookies at the start of every task:
- If valid → skip login, open Steam directly
- If expired → full login, store fresh cookies, overwrite the old file
Expired cookies are overwritten immediately — no stale files hanging around.
Multi-Machine Architecture
This was the most complex part. The setup spanned 5 machines:
| Machine | OS | Role |
|---|---|---|
| PC | Windows | CS2 game + Steam client |
| VM 1 | Linux | Backend API |
| VM 2 | Linux | Frontend dashboard |
| VM 3 | Linux | Database |
| VM 4 | Linux | Worker tasks |
The Windows PC was Karol's main machine where Steam and CS2 were installed. The backend on VM1 was built with Python + FastAPI — it handled all the logic: sending commands to launch the game, checking cookie expiry, and serving data to the dashboard. The frontend on VM2 was built with React + Vite for fast development and hot reloads, styled with Tailwind CSS for a clean, responsive UI. VM3 ran the database, and VM4 handled auxiliary scraping tasks.
Connecting Everything
SSH keys handled authentication between machines. Docker containerized each service so deployment was consistent:
# docker-compose.yml (simplified)
services:
backend:
build: ./backend # FastAPI + Python
ports:
- "8000:8000"
environment:
- DB_HOST=db
- SSH_KEY_PATH=/keys/id_rsa
volumes:
- ./keys:/keys
- ./cookies:/cookies
frontend:
build: ./frontend
ports:
- "3000:3000"
depends_on:
- backend
db:
image: postgres:15
volumes:
- pgdata:/var/lib/postgresql/data
Remote Game Control
The backend connected to the Windows PC via SSH to execute game commands:
import paramiko
ssh = paramiko.SSHClient()
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
ssh.connect("windows-pc-ip", username="karol", key_filename="/keys/id_rsa")
# Launch CS2
stdin, stdout, stderr = ssh.exec_command(
'start steam://rungameid/730'
)
# Wait for game window, then send keystrokes for Deathmatch
# This was done via a helper script on the Windows machine
The Client Dashboard
Karol could manage everything from a web dashboard built with React + Vite and Tailwind CSS (chosen specifically for fast iteration — Vite's instant hot reload made UI tweaks painless):
- View task status (running, completed, failed)
- Monitor cookie expiry dates
- See scraped XP/level data in real-time
- Manually trigger tasks or schedule them
- View logs from all 5 machines in one place
What I Learned
- Cookie management is the silent killer — Steam sessions last long but fail unpredictably. Always check before assuming.
- Selenium is still the right choice for auth-heavy automation — Playwright is great, but Selenium's cookie and profile handling is more battle-tested.
- Multi-VM debugging is painful without centralized logging — Set up log aggregation early, not after the first crash.
- SSH + Docker is a powerful combo — lightweight, secure, and works across Windows and Linux seamlessly.
- Windows game automation from Linux is possible — but requires careful SSH command construction and sometimes a helper agent on the Windows side.
This was one of those projects where the simple-sounding requirement ("automate CS2") turned into a distributed systems challenge. But getting it working — seeing the dashboard show live XP data scraped automatically from a game running on another machine — was worth every debugging session.