Turn your smartphone into a powerful remote control for your computer.
Touchpad · Keyboard · Live Screen Streaming · Terminal · Process Manager · File Browser
No cloud. No account. No agent phoning home. Your phone talks directly to your computer over your own Wi-Fi.
- Why SPIDER_CTRL?
- Features Overview
- Live App Interface Gallery
- Real-World Use Cases
- Architecture
- Pairing & Authentication
- Granting Input Permission
- Quick Start Guide
- Detailed Installation Guide
- Connecting Your Phone
- Install as an App (PWA)
- Enabling HTTPS/TLS
- Deploying the Frontend to Vercel
- Platform Support Matrix
- Configuration Reference
- API Reference
- Project Structure
- Security Model
- Uninstalling
- Testing & Self-Check
- Troubleshooting
- Development Guide
- Contributing
- FAQ
- License
Most remote desktop solutions are overkill, require installing software on both devices, or route your data through cloud servers you don't control.
SPIDER_CTRL is different:
| SPIDER_CTRL | TeamViewer / AnyDesk | Chrome Remote Desktop | |
|---|---|---|---|
| Requires account | ❌ No | ✅ Yes | ✅ Yes |
| Cloud relay | ❌ Direct LAN | ✅ Cloud servers | ✅ Google servers |
| Install on phone | ❌ Just a browser | ✅ App required | ✅ App required |
| Open source | ✅ MIT | ❌ Proprietary | ❌ Proprietary |
| Works offline | ✅ LAN only | ❌ Needs internet | ❌ Needs internet |
| Data stays local | ✅ Always | ❌ Routed through cloud | ❌ Routed through cloud |
| Custom automation | ✅ WebSocket API | ❌ | ❌ |
| Latency | < 10ms (LAN) | 50–200ms | 50–150ms |
Zero install on the phone. Open a URL in your mobile browser — that's it. Install as a PWA for a native-app experience with its own icon and no browser chrome.
Privacy first. All traffic stays on your local network. No cloud relay, no telemetry, no account. You can verify this — the source code is right here.
The interface has five tabs across the bottom: PAD, KEYS, STRM, TOOLS, SYS.
Direct screen captures from the live SPIDER_CTRL mobile client running on a phone connected to the host system over local Wi-Fi:
Left to Right: 1. Multi-Touch Trackpad · 2. Virtual Keyboard & Macros · 3. Volume Dial & Media Transport · 4. Remote Shell Terminal
Left to Right: 5. Real-Time Process Manager · 6. File Browser · 7. Quick App Launcher · 8. Power & System Controls
A responsive trackpad that mirrors laptop-grade gestures on your phone screen.
| Gesture | Action |
|---|---|
| One finger drag | Move the cursor |
| One finger tap | Left click |
| Two finger drag | Scroll (vertical and horizontal) |
| Two finger tap | Right click |
| Three finger tap | Middle click |
- Dedicated
L_CLICK/ middle /R_CLICKbuttons for precision work - Relative movement — works regardless of screen dimensions
- Configurable sensitivity (
SENSITIVITYinTouchpad.tsx, default 1.8) - Jump re-anchoring prevents cursor teleportation when lifting and re-placing fingers
- Peak finger detection for accurate gesture recognition
- Live text input — every character streams instantly to the host
- Unicode support — emoji, accents, CJK characters auto-route through clipboard
- Modifier keys — Esc, Tab, Ctrl, Alt, Win/⌘, Shift, arrows
- Function row — F1 through F12
- One-tap macros — Copy, Paste, Cut, Undo, Select All, Alt+Tab, Alt+F4, Win+D, Ctrl+Shift+Esc, PrintScreen
- macOS-aware —
CtrlandWinautomatically remap toCommandon macOS hosts
Real-time desktop streaming via WebRTC — not screenshot polling, real video.
| Preset | Resolution | FPS | Target Bitrate |
|---|---|---|---|
| 🟢 Low | 640 × 360 | 15 | 500 kbps |
| 🟡 Medium | 960 × 540 | 24 | 1200 kbps |
| 🟠 High | 1280 × 720 | 30 | 2500 kbps |
| 🔴 Ultra | 1920 × 1080 | 30 | 4000 kbps |
- Adaptive quality — auto-adjusts based on real-time network RTT measurements
- Live stats overlay — latency, FPS, resolution, bitrate at a glance
- Fullscreen with landscape orientation lock on mobile
- Aspect-ratio preserving — 16:10, 4:3, and portrait monitors stream undistorted
- Auto-reconnect — up to 5 retries if the stream drops
- Multi-viewer — multiple phones can stream simultaneously with independent quality
Six sub-tabs: SRCH, APPS, MEDIA, SHELL, PROC, FS.
Open any URL or run a Google search directly in the host's default browser. Recent entries are remembered locally. Bare hostnames get https:// prepended automatically.
A searchable grid of 30+ preconfigured apps per platform:
- Windows: Notepad, Calculator, Chrome, VS Code, Spotify, Discord, Steam, Terminal, Office apps, and more
- macOS: Finder, Safari, Chrome, iTerm, Activity Monitor, System Settings, Music, and more
- Linux: Nautilus, Firefox, Chrome, gnome-terminal, VS Code, VLC, LibreOffice, and more
Anything not in the list can be launched by typing its command directly.
Circular volume dial with ±5% step controls and mute toggle. Transport controls for play/pause, next, previous, and stop — emits real media keys, works with Spotify, YouTube, VLC, or any focused app.
A full shell on the host machine:
- Persistent working directory —
cdsticks between commands - Command history — ↑ / ↓ recall (last 50 commands)
- Separated output — stdout and stderr rendered distinctly, with exit codes
- Timeouts — 30s default (120s max), 64KB output cap per stream
- Home-relative paths —
C:\Users\you\srcdisplays as~/src
Live process table refreshing every 5 seconds:
- Sort by memory, CPU, name, or PID
- Filter by name
- Per-process detail: executable path, command line, thread count, user, start time
- Graceful terminate (SIGTERM) or force kill (SIGKILL)
- System summary: CPU %, core count, RAM used/total
- PIDs 0 and 4 are protected from being killed
Read-only filesystem browser:
- Drive and mount-point picker (
C:\,D:\on Windows;/,/home,/mnton Unix) - Folders first, then alphabetical; sizes and modified timestamps
- Per-extension icons for code, documents, images, video, audio, and archives
- Symlinks flagged; hidden and system files filtered by default
Safe by design — file contents are never transmitted. Nothing can be created, moved, or deleted.
Shutdown, Restart, Sleep, Lock Screen, and Log Out — each behind a confirmation step. Works on Windows, macOS, and Linux. The host's OS, hostname, CPU, RAM, and battery level are shown alongside.
Read and write the host clipboard remotely:
- Windows: PowerShell
- macOS:
pbcopy/pbpaste - Linux:
wl-clipboard,xclip, orxsel
The server broadcasts a UDP beacon on port 8766 every 2 seconds. When you open the UI from the same network, the app can auto-detect and connect — no IP to type.
Control your presentation slides wirelessly from your phone. Walk around the room while advancing slides, switching apps, or adjusting volume — no need for a $50 Logitech clicker.
Example workflow: Stand at the podium → open SPIDER_CTRL on your phone → use the trackpad to advance PowerPoint slides → switch to a demo app → adjust volume for a video — all from your phone.
Turn your HTPC or connected laptop into a media center controlled from the couch. Browse media files, launch VLC/Spotify, adjust volume, and control playback without getting up.
Example workflow: Couch → launch Spotify from the app launcher → control volume with the media dial → skip tracks → search for a YouTube video → stream it on the big screen.
Monitor server processes, check system resources, kill hanging processes, and execute diagnostic commands — all from your phone while walking between server racks.
Example workflow: Walk to server rack → open Process Manager → sort by CPU usage → identify the runaway process → kill it → run a diagnostic command in the terminal → verify the fix.
Use your phone as an adaptive input device when a traditional mouse or keyboard isn't practical. The touchpad works as a high-precision pointer, and the virtual keyboard handles full text input.
Your computer is in the office, but you need to check something while you're in the kitchen. No need to walk back — pull out your phone, stream the screen, and handle it remotely.
Control OBS scene switching, adjust audio levels, or manage your streaming overlay from your phone while gaming. Keep your hands on the keyboard and mouse while using the phone for auxiliary controls.
Run build commands, check logs, restart services, or browse project files from your phone while stepping away from your desk. The remote terminal supports full command execution.
Example workflow: Step away from desk → run
npm run buildin the terminal → check the output → browse the/distfolder in the file browser → verify the build.
| Layer | Technology | Runs On |
|---|---|---|
| UI Framework | Next.js 14 (App Router) + React 18 + TypeScript | Phone browser |
| Styling | Tailwind CSS + Framer Motion, JetBrains Mono | Phone browser |
| Control Channel | WebSocket (JSON request/response) | LAN |
| Video Channel | WebRTC (aiortc ⇄ browser), H.264/VP8 | LAN |
| Server | FastAPI + Uvicorn (Python) | Host PC |
| Input Automation | pyautogui | Host PC |
| Screen Capture | mss + NumPy + PyAV | Host PC |
| System Control | psutil, pycaw (Win) / osascript (Mac) / pactl (Linux) | Host PC |
WebSocket for control — small, ordered messages that need replies (mouse moves, key presses, shell commands).
WebRTC for video — continuous high-bandwidth stream that tolerates packet loss, with built-in NAT traversal and congestion control.
The Python server both handles API requests AND serves the frontend as static files. A phone on the same network just browses to http://<pc-ip>:8765 — nothing else to deploy.
📐 ARCHITECTURE.md goes deeper: the browser rules that shaped the design, why there is no cloud broker, the pairing threat model, connection sequence diagrams, and the full cost/licence breakdown.
Every device pairs once. Pairing hands the phone a 256-bit token that it stores and reuses, so this only happens the first time.
┌─ On the PC ────────────────────────────────────────────────┐
│ 1. Open the site (deployed, or http://localhost:8765) │
│ 2. Not installed? Copy the one-line install command │
│ 3. The server starts and the page shows a QR code │
└────────────────────────────────────────────────────────────┘
│
┌─ On the phone ─────────────────────────────────────────────┐
│ 4. Scan the QR with the camera, open the link │
│ 5. Paired and connected — nothing typed │
└────────────────────────────────────────────────────────────┘
The server prints the same QR code in its terminal, so the flow works even if the setup page can't reach the server (Safari blocks page→localhost requests).
Browse to the address the server printed and enter the six-character pairing code shown on the PC. The code works once and expires after 10 minutes — restart the server for a fresh one.
The connector page shows the right command for your OS:
irm https://spider-ctrl.vercel.app/install.ps1 | iexcurl -fsSL https://spider-ctrl.vercel.app/install.sh | shEach clones the repo to ~/.spider-ctrl, builds the UI, creates the virtualenv,
opens the firewall port where it can, and starts the server. Re-running updates
in place. Requires Python 3.9+, Node 18+ and git.
| Task | How |
|---|---|
| Pair a new device | Scan the QR, or enter the pairing code |
| Unpair one device | Tap forget this pc in the app's connection panel |
| Unpair everything | POST /pair/rotate from the PC, or delete server/config/pairing.json. Devices that are connected right now are kicked immediately |
The token lives in server/config/pairing.json (gitignored, chmod 600 on
POSIX). It is stripped from the phone's address bar immediately after it is
stored, so it never lingers in history or screenshots.
Important
macOS and Linux need one permission before the trackpad and keyboard do anything. Without it the phone still pairs and connects, every button still lights up, and nothing moves — the OS discards the synthetic events and pyautogui does not report an error.
SPIDER_CTRL detects this now. The server prints a warning on startup and the phone shows an amber INPUT BLOCKED ON THE PC banner with the exact fix.
- System Settings → Privacy & Security → Accessibility
- Switch ON the app you start the server from — Terminal, iTerm, or whichever app you launch it in
- Quit the server and start it again — the permission is only read when a process starts
macOS grants Accessibility to the responsible process, which for a command-line server is the terminal app, not the Python binary. Adding
python3alone usually will not work. If you launch the server some other way (a LaunchAgent, a bundled app), add the interpreter the warning prints.
Screen streaming additionally needs Screen Recording for the same app.
pyautogui drives X11. A pure Wayland session blocks synthetic input outright — log into an Xorg session instead. The server reports this too.
Nothing to grant. If the target window ignores input it is usually running elevated, so run the server as administrator too.
curl -s http://localhost:8765/health | grep input_ok"input_ok": true means the OS will accept input. The phone can also re-check
from the banner without restarting anything.
Get running in under 2 minutes.
| Component | Requirement |
|---|---|
| Host PC | Python 3.10+ (3.11+ recommended) |
| Frontend Build | Node.js 18+ and npm |
| Operating System | Windows 10/11, macOS 11+, or Linux (X11/Wayland) |
| Phone | Any modern browser (Chrome, Safari, Firefox, Edge) |
| Network | Same Wi-Fi, LAN, or hotspot |
🪟 Windows
git clone https://github.com/muneebkhan08/test_mob_ctrl.git
cd test_mob_ctrl
start-server.batThe batch file handles venv creation, dependency installation, frontend build, and server launch automatically.
🍎 macOS
git clone https://github.com/muneebkhan08/test_mob_ctrl.git
cd test_mob_ctrl
chmod +x start-server.sh
./start-server.shFirst time on macOS? You'll need to grant Accessibility and Screen Recording permissions: System Settings → Privacy & Security → Accessibility → add Terminal/Python. System Settings → Privacy & Security → Screen Recording → add Terminal/Python.
🐧 Linux
git clone https://github.com/muneebkhan08/test_mob_ctrl.git
cd test_mob_ctrl
chmod +x start-server.sh
./start-server.shNote: X11 works out of the box. Wayland restricts synthetic input — run under XWayland or an X11 session for full functionality.
════════════════════════════════════════════════════════
🖥️ SPIDER_CTRL Server v1.0.0
🔌 WebSocket → ws://192.168.1.42:8765/ws
🎥 WebRTC → /webrtc/offer
📡 UDP Disco → port 8766
🔒 TLS → disabled (set SPIDER_CTRL_TLS=1)
💻 Platform → Windows 10
════════════════════════════════════════════════════════
📱 Scan this with your phone's camera:
█▀▀▀▀▀▀▀█▀█▀█▀▀▀▀█▀▀▀█▀▀▀███▀██▀▀▀▀▀▀▀█
█ █▀▀▀█ █▀█ ▄████▄▀█▄█▀ █▄ █▀██ █▀▀▀█ █
█ █ █ ██ ▄█▄█ ▄ ▀ ▄▄█▄▄ ▀█ █ █ █ █
█ ▀▀▀▀▀ █ █ █▀▄ ▄▀█▀█ ▄ █ ▄▀█▀█ ▀▀▀▀▀ █
█▀█▀███▀▀██▀▄▀███▀▀ ███▄▄▄██▀▀██▀██▀█▀█
… (full QR) …
▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀
🔗 Or open on phone → http://192.168.1.42:8765
🔑 Pairing code → 8PB-5P8
🖥️ Setup screen → http://localhost:8765/
════════════════════════════════════════════════════════
Scan the QR with your phone's camera and open the link. That's it — the QR carries the pairing token, so the phone pairs and connects in one step.
First run on macOS or Linux? Check everything is wired up before you rely on it:
cd server && venv/bin/python selftest.pyMouse and keyboard fail until you grant input permission — see Granting Input Permission.
The setup screen also opens in your browser automatically, showing the same QR at a comfortable size.
Step-by-step for all platforms.
git clone https://github.com/muneebkhan08/test_mob_ctrl.git
cd test_mob_ctrlcd frontend
npm install
npm run build # Creates static export in frontend/out/
cd ..The
npm run buildcommand produces a static export of the Next.js app. This is what the Python server serves to your phone.
cd server
# Create a virtual environment
python -m venv venv
# Activate it
# Windows:
venv\Scripts\activate
# macOS / Linux:
source venv/bin/activate
# Install dependencies
pip install -r requirements.txtpython server.py- Make sure your phone is on the same Wi-Fi network as your computer
- Scan the QR code the server prints on startup — the setup screen also opens in your browser automatically with the same code
- That's it. The QR carries the pairing token, so the phone pairs and connects in one step with nothing to type
No camera? Open the address the server prints (e.g. http://192.168.1.42:8765)
and enter the six-character pairing code shown on the PC.
cd test_mob_ctrl
git pull
cd frontend && npm install && npm run build && cd ..
cd server && pip install -r requirements.txt && cd ..Every device has to pair once before it can control the PC. Pairing hands the phone a token that it stores and reuses, so this only happens the first time.
Both devices on the same router. Point the phone's camera at the QR code — in
the server's terminal, or on the setup screen that opens at
http://localhost:8765. Open the link and you're connected.
If the camera isn't an option, browse to the address the server printed and type the six-character code from the PC. The code works once and expires after ten minutes; restart the server for a fresh one.
- Windows: Settings → Network → Mobile Hotspot → On macOS: System Settings → General → Sharing → Internet Sharing
- Connect the phone to that hotspot
- Use the host's hotspot IP (usually
192.168.137.1on Windows)
- Enable the hotspot on your phone
- Connect the PC to it
- Start the server and use whichever IP it prints
ipconfig # Windows
ifconfig | grep "inet " # macOS
hostname -I # LinuxSPIDER_CTRL is a Progressive Web App — install it on your phone for a native-app experience:
iOS (Safari):
- Open the server URL in Safari
- Tap the Share button (⬆️)
- Tap Add to Home Screen
- Tap Add
Android (Chrome):
- Open the server URL in Chrome
- Tap the ⋮ menu
- Tap Add to Home screen or Install app
Once installed, it launches fullscreen with no browser chrome, its own icon, and the dark theme applied to the status bar.
SPIDER_CTRL supports optional TLS encryption for secure connections:
# Enable TLS with a single environment variable
SPIDER_CTRL_TLS=1 python server.pyWhat happens:
- A self-signed SSL certificate is auto-generated with your local IP in the SAN
- The certificate regenerates automatically when your IP changes
- The server starts on
https://andwss://instead of plain HTTP - You'll need to accept the self-signed certificate warning on your phone (once per IP change)
On Windows:
set SPIDER_CTRL_TLS=1
python server.pyNote: TLS encrypts the channel but doesn't authenticate clients. For trusted networks, plain HTTP is fine. Enable TLS if you want encryption on the wire.
Optional. Hosting the UI on Vercel gives you a stable URL to bookmark instead of a changing IP.
- Push the repo to GitHub
- Vercel → Add New → Project → import the repo
- Set Root Directory to
frontend(critical — the repo root has nopackage.json) - Framework preset:
Next.js. Leave all other settings at defaults. - Deploy
How it works: The Vercel-hosted page acts as a launcher. You type the host IP, tap Go, and it redirects to http://<ip>:8765 where the server handles everything same-origin. The last IP is remembered.
See DEPLOYMENT.md for the click-by-click walkthrough.
| Feature | Windows | macOS | Linux |
|---|---|---|---|
| Mouse / Keyboard | ✅ | ✅ ¹ | ✅ ² |
| Screen Streaming | ✅ | ✅ ¹ | ✅ |
| Terminal | ✅ | ✅ | ✅ |
| Process Manager | ✅ | ✅ | ✅ |
| File Browser | ✅ | ✅ | ✅ |
| App Launcher | ✅ | ✅ | ✅ |
| Power Controls | ✅ | ✅ | ✅ |
| Volume | ✅ pycaw | ✅ osascript | ✅ pactl |
| Media Keys | ✅ | ✅ | |
| Clipboard | ✅ | ✅ | ✅ ⁴ |
¹ macOS needs Accessibility and Screen Recording permissions for the terminal or Python binary: System Settings → Privacy & Security.
² X11 works out of the box. Wayland restricts synthetic input; run under XWayland or an X11 session.
³ Media keys depend on the desktop environment honouring XF86 key symbols.
⁴ Linux needs
wl-clipboard,xclip, orxselinstalled.
pycaw and comtypes are marked sys_platform == "win32" in requirements.txt, so pip install -r requirements.txt is safe on all platforms.
Constants at the top of server/server.py:
| Constant | Default | Description |
|---|---|---|
SERVER_PORT |
8765 |
HTTP + WebSocket port |
UDP_PORT |
8766 |
Discovery broadcast port |
BROADCAST_INTERVAL |
2 |
Seconds between UDP beacons |
| Variable | Values | Description |
|---|---|---|
SPIDER_CTRL_TLS |
1, true, yes |
Enable HTTPS/WSS with auto-generated self-signed certificates |
SPIDER_CTRL_ORIGINS |
Comma-separated origins | Hosted copies of the UI allowed to read /pair. Default: https://spider-ctrl.vercel.app |
SPIDER_CTRL_NO_BROWSER |
1, true, yes |
Don't open the setup page on boot (headless / service installs) |
Warning
SPIDER_CTRL_ORIGINS is exact match only — set it to your real deployment
URL. The /pair response carries the pairing token, so a wildcard here would
let any site the user visits read it and take over the machine.
| File | Constant | Default | Description |
|---|---|---|---|
controllers/terminal.py |
DEFAULT_TIMEOUT |
30s | Shell command timeout |
controllers/terminal.py |
MAX_TIMEOUT |
120s | Maximum allowed timeout |
controllers/terminal.py |
MAX_OUTPUT_BYTES |
64 KB | Output cap per stream |
controllers/screen.py |
QUALITY_PRESETS |
see table above | WebRTC stream quality |
utils/pairing.py |
CODE_TTL |
600s | Pairing-code lifetime |
utils/pairing.py |
CODE_MAX_ATTEMPTS |
5 | Wrong guesses before the code is destroyed |
utils/pairing.py |
CODE_LENGTH |
6 | Characters in the pairing code |
| File | Constant | Default | Description |
|---|---|---|---|
Touchpad.tsx |
SENSITIVITY |
1.8 |
Trackpad sensitivity multiplier |
useWebSocket.tsx |
RECONNECT_DELAY |
3000 ms | WebSocket reconnect interval |
Theme colours are defined in two places — change both to restyle:
- CSS variables in
app/globals.css - Tailwind tokens in
tailwind.config.js
Endpoint: ws://<host>:8765/ws
All frames are JSON. Request-response pairing uses an optional id field.
Request format:
Response format:
{ "ok": true, "data": { "moved": [10, -5] }, "id": "req_1" }
{ "ok": false, "error": "Process 1234 does not exist", "id": "req_1" }Performance tip: Fire-and-forget messages (cursor movement, key presses) omit
idand get no reply, keeping the trackpad smooth at 60+ events/second.
🖱️ Mouse Actions
| Action | Payload | Description |
|---|---|---|
mouse_move |
{dx, dy} |
Relative cursor move |
mouse_click |
{button} — left|right|middle |
Click |
mouse_double_click |
— | Double click |
mouse_right_click |
— | Right click |
mouse_scroll |
{dx, dy} |
Scroll both axes |
mouse_drag_start |
— | Press and hold |
mouse_drag_move |
{dx, dy} |
Move while held |
mouse_drag_end |
— | Release |
⌨️ Keyboard Actions
| Action | Payload | Description |
|---|---|---|
key_press |
{key} |
Press one key |
key_combo |
{keys: [...]} |
Chord, e.g. ["ctrl","shift","esc"] |
key_type |
{text} |
Type a string (unicode via clipboard) |
⚡ Power Actions
| Action | Payload | Description |
|---|---|---|
power_shutdown |
{delay?} |
Shut down after delay seconds |
power_restart |
{delay?} |
Restart |
power_sleep |
— | Suspend |
power_lock |
— | Lock the screen |
power_logout |
— | Log the user out |
📱 App & Search Actions
| Action | Payload | Description |
|---|---|---|
app_open |
{name} or {path} |
Launch by friendly name or command |
app_list |
— | Quick-launch names + running processes |
app_close |
{name} or {pid} |
Terminate |
google_search |
{query} |
Search in the default browser |
url_open |
{url} |
Open a URL |
🔊 Volume & Media Actions
| Action | Payload | Description |
|---|---|---|
volume_get |
— | {volume, muted} |
volume_set |
{level} |
Set 0–100 |
volume_up / volume_down |
{step?} |
Step, default 5 |
volume_mute |
— | Toggle mute |
media_play_pause |
— | Play / pause |
media_next / media_prev |
— | Track change |
media_stop |
— | Stop |
💻 Terminal Actions
| Action | Payload | Description |
|---|---|---|
terminal_execute |
{command, timeout?} |
{stdout, stderr, exit_code, cwd} |
terminal_cwd |
— | Current directory |
terminal_set_cwd |
{path} |
Change directory |
terminal_reset |
— | Back to home |
⚙️ Process Actions
| Action | Payload | Description |
|---|---|---|
process_list |
{sort_by?, limit?, search?} |
Processes + system summary |
process_detail |
{pid} |
exe, cmdline, threads, user, start time |
process_kill |
{pid, force?} |
SIGTERM, or SIGKILL when force |
📁 Filesystem Actions
| Action | Payload | Description |
|---|---|---|
fs_list |
{path?, show_hidden?} |
Entries with metadata |
fs_drives |
— | Drives / mount points |
fs_info |
{path} |
Single-entry detail |
📋 Clipboard & System Info
| Action | Payload | Description |
|---|---|---|
clipboard_get |
— | {text} |
clipboard_set |
{text} |
{copied: true} |
system_info |
— | Host, OS, CPU, RAM, battery |
input_status |
— | {ok, reason, fix, binary, platform} — whether the OS will accept synthetic input |
| Method | Path | Purpose |
|---|---|---|
GET |
/health |
Server status, IP, platform, active stream count, input_ok |
GET |
/pair |
Loopback only. Token, QR (SVG data URI), pairing code, address |
POST |
/pair/claim |
Trade a six-character code for the token — {code} → {token, ip, port} |
GET |
/uninstall/plan |
Loopback only. What deleting this install would remove, with sizes. Changes nothing |
POST |
/uninstall |
Loopback only. Deletes this install. Requires {"confirm":"DELETE"}; the server stops afterwards |
GET |
/uninstall/scan |
Loopback only. Every SPIDER_CTRL install found (default location + any ?path=), each with the same shape as /uninstall/plan |
POST |
/uninstall/remove |
Loopback only. Deletes the install at {"root": "...", "confirm": "DELETE"}. Never stops the server unless root is its own tree |
POST |
/pair/rotate |
Loopback only. New token; unpairs every device and disconnects any that are live. Returns {ok, url, disconnected} |
GET |
/{path} |
Static frontend (SPA fallback to index.html) |
WebSocket auth: connect to ws://<host>:8765/ws?t=<token>. A missing,
wrong or rotated token is closed with code 1008 — distinct from a dropped
connection, so the client knows to re-pair instead of retrying forever.
| Method | Path | Body | Returns |
|---|---|---|---|
POST |
/webrtc/offer |
{sdp, type, quality} |
{connection_id, sdp, type, quality} |
POST |
/webrtc/ice |
{connection_id, candidate} |
{ok} |
POST |
/webrtc/quality |
{connection_id, quality} |
{ok, quality} |
POST |
/webrtc/stop |
{connection_id} |
{ok} |
GET |
/webrtc/stats/{id} |
— | Connection and ICE state |
Flow: Browser creates offer → POST /webrtc/offer → server attaches screen capture track and answers → ICE candidates trickle both ways → video flows. A data channel named stats carries RTT reports and quality changes without HTTP round-trips.
The server broadcasts to port 8766 every 2 seconds:
{
"service": "SPIDER_CTRL Server",
"version": "1.0.0",
"ip": "192.168.1.42",
"port": 8765,
"hostname": "DESKTOP-ABC",
"platform": "Windows"
}test_mob_ctrl/
├── assets/ # README images
├── frontend/ # Next.js PWA (static export)
│ ├── app/
│ │ ├── page.tsx # Tab shell and layout
│ │ ├── layout.tsx # Metadata, PWA manifest, icons
│ │ ├── globals.css # Cyberpunk theme, animations
│ │ ├── hooks/
│ │ │ ├── useWebSocket.tsx # Control channel, reconnect, req/res
│ │ │ └── useWebRTC.tsx # Video channel, signalling, stats
│ │ └── components/
│ │ ├── PCConnector.tsx # Desktop setup: install + pairing QR
│ │ ├── PairPrompt.tsx # Phone: pairing-code entry
│ │ ├── InputWarning.tsx # Warns when the OS blocks input
│ │ ├── UninstallPanel.tsx # Danger zone: remove from this PC
│ │ ├── OtherInstalls.tsx # Find + remove any install on the machine
│ │ ├── ConnectionBar.tsx # Connect / redirect / status
│ │ ├── Touchpad.tsx # Multi-touch trackpad
│ │ ├── Keyboard.tsx # Text, keys, macros
│ │ ├── ScreenViewer.tsx # Video surface + quality UI
│ │ ├── RemoteTerminal.tsx # Shell emulator
│ │ ├── ProcessManager.tsx # Process table
│ │ ├── FileBrowser.tsx # Filesystem browser
│ │ ├── AppLauncher.tsx # App grid
│ │ ├── GoogleSearch.tsx # Search / URL
│ │ ├── MediaVolume.tsx # Volume + transport
│ │ └── PowerControls.tsx # Power actions
│ ├── public/ # manifest.json, PWA icons
│ │ ├── install.sh # macOS / Linux one-line installer
│ │ └── install.ps1 # Windows one-line installer
│ └── out/ # Build output — served by the server
│
├── server/ # FastAPI server (runs on the host)
│ ├── server.py # App, WS endpoint, dispatch, UDP beacon
│ ├── selftest.py # Health check for every subsystem
│ ├── requirements.txt # Python dependencies with platform markers
│ ├── requirements-dev.txt # pytest, for the test suite
│ ├── tests/
│ │ ├── test_pairing.py # Token, code and origin-policy tests
│ │ ├── test_server_auth.py # Route-level auth and revocation
│ │ ├── test_permissions.py # Input-permission detection
│ │ └── test_uninstall.py # Uninstall safety (mostly refusals)
│ ├── controllers/
│ │ ├── mouse.py # Move, click, scroll, drag
│ │ ├── keyboard.py # Keys, combos, text, unicode
│ │ ├── screen.py # Capture + WebRTC track + quality
│ │ ├── terminal.py # Shell with persistent cwd
│ │ ├── processes.py # List / inspect / kill
│ │ ├── filesystem.py # Read-only browsing
│ │ ├── apps.py # Launch and close apps (cross-platform)
│ │ ├── power.py # Shutdown, restart, sleep, lock (cross-platform)
│ │ ├── volume.py # Per-platform volume control
│ │ ├── media.py # Media keys
│ │ ├── clipboard.py # Per-platform clipboard
│ │ ├── search.py # Google / URL
│ │ └── system_info.py # CPU, RAM, battery
│ ├── utils/
│ │ ├── pairing.py # Tokens, pairing codes, origin policy, QR
│ │ ├── permissions.py # Can the OS actually accept input?
│ │ ├── uninstall.py # Removal planning + guards
│ │ ├── network.py # LAN IP detection
│ │ └── ssl_cert.py # Self-signed cert generator
│ ├── config/ # Pairing token — generated, gitignored
│ └── certs/ # Generated at runtime — gitignored
│
├── start-server.bat # Windows launcher
├── start-server.sh # macOS / Linux launcher
├── ARCHITECTURE.md # Design deep-dive and threat model
├── DEPLOYMENT.md # Vercel deployment walkthrough
└── README.md
Important
Every connection requires a pairing token. An unpaired device on your network cannot connect, and cannot run a single command. Pairing is described in Pairing & Authentication.
| Mechanism | What it stops |
|---|---|
256-bit token on every /ws, compared with hmac.compare_digest |
Any unpaired device, including one on loopback |
| Origin port check on the WebSocket handshake | Hostile web pages and DNS rebinding — an attacker's site is served from :80/:443, never :8765 |
/pair is loopback-only |
Other devices on the Wi-Fi simply asking for the token |
| Exact-match CORS allowlist | A website you have open reading the token — its request comes from loopback too, so the loopback check alone is not enough |
| Private Network Access opt-in | Adds Chrome's own gate in front of the above |
| Code expiry, single use, 5-attempt cap | Brute-forcing the short pairing code |
Note
WebSocket handshakes are not subject to CORS. That is why the token is
required even for connections from 127.0.0.1 — without it, any page you had
open could open a socket to the server and take over the machine.
Pairing is all-or-nothing. Once a device holds the token it can:
- Run arbitrary shell commands as the user running the server (
terminal_execute) - Kill any process that user owns (
process_kill) - Shut down, restart or lock the machine, with no confirmation prompt
- Browse the filesystem (names and metadata, not contents)
- Watch the screen
So treat the token like a password, and only pair devices you control.
- The server binds
0.0.0.0— the port is reachable from the whole subnet, even though connecting requires the token - Traffic is plaintext by default (enable TLS with
SPIDER_CTRL_TLS=1) - The UDP beacon on 8766 advertises the server's presence to the subnet
- There is no per-device revocation — rotating drops every device (connected ones are disconnected immediately) and they all have to pair again
- There is no approval prompt on the PC; the token is the only gate
| ✅ Safe | ❌ Unsafe |
|---|---|
| Home Wi-Fi with WPA2/WPA3 | Café, airport, hotel Wi-Fi |
| Your own hotspot | Campus Wi-Fi with guests |
| Isolated LAN | Port forwarding to the internet |
- Keep the token secret — anyone holding it has full control; rotate with
/pair/rotateif a device is lost - Run only while using it — the UDP beacon advertises your server to the entire subnet
- Firewall it — allow 8765/tcp and 8766/udp on private profiles only
- Enable TLS —
SPIDER_CTRL_TLS=1encrypts traffic with auto-generated certs - Run as unprivileged user — don't launch from an admin/root shell
- Don't tunnel it — no ngrok, no Tailscale funnel, no port forwarding
Windows firewall rules (elevated PowerShell):
New-NetFirewallRule -DisplayName "SPIDER_CTRL" -Direction Inbound -Protocol TCP -LocalPort 8765 -Profile Private -Action Allow
New-NetFirewallRule -DisplayName "SPIDER_CTRL Disco" -Direction Inbound -Protocol UDP -LocalPort 8766 -Profile Private -Action AllowThe repo's first commit included a self-signed key.pem. Those files are no longer tracked, and .gitignore now covers them. They remain in git history — use git filter-repo to purge if needed. The practical impact is low as the key was never wired into the running server.
Run the self-test. It checks every subsystem and tells you exactly what to fix.
cd server
venv/bin/python selftest.py── Input control ───────────────────────────────────────
[PASS] Mouse control
cursor moved and was restored
[PASS] Keyboard control
same event path as the mouse — available
It is read-only apart from a one-pixel cursor nudge it puts straight back, and it exits non-zero on failure so you can gate a script with it. Checks:
| Group | What it verifies |
|---|---|
| Environment | Python 3.9+, all 11 runtime packages, frontend actually built |
| Pairing | Token creates, persists and verifies; code issues; QR renders both ways |
| Input control | Moves the cursor and reads the position back — the real proof, not just the permission flag |
| Screen | Captures a frame, and warns if it comes back blank (missing Screen Recording) |
| Media keys | The platform's media-key mechanism is available (checked without firing a key) |
| Controllers | system_info, filesystem, processes, clipboard, volume, terminal each respond |
| Network | A real LAN address exists, and whether 8765 is free |
Run it once after installing, and again after granting input permission. Mouse and keyboard are the two checks that fail on a fresh macOS install — see Granting Input Permission.
The pairing and uninstall layers decide who can drive your machine and what gets deleted, so both ship with tests — mostly negative ones, because a regression there is a stranger getting a shell, or your source tree going in the bin.
cd server
venv/bin/pip install -r requirements-dev.txt
venv/bin/python -m pytest tests/ -v108 cases across six files:
| File | Cases | Covers |
|---|---|---|
test_pairing.py |
24 | Token lifecycle and rotation; pairing codes (no ambiguous characters, dash/case tolerance, single use, expiry, destroyed after the attempt cap); origin policy — LAN, loopback and allowlisted origins pass, hostile sites, suffix look-alikes (…vercel.app.evil.com), other local ports and non-HTTP schemes are rejected; QR rendering |
test_server_auth.py |
23 | The routes actually apply the policy: /pair loopback-only and origin-gated, PNA preflight, code exchange, WebSocket auth, an unpaired socket cannot run a command, and rotation revoking a live connection |
test_permissions.py |
14 | Input detection per platform, a blocked host reported with a fix, an unverifiable check not crying wolf, and a guard against reintroducing the prompt that once segfaulted the interpreter |
test_media.py |
9 | The macOS/other-platform split for media keys, and that macOS never falls through to pyautogui — whose media keys are a silent no-op there |
test_selftest.py |
5 | The diagnostic itself runs to completion, reports every group, leaves nothing in an unknown state, and never fails without saying why |
test_uninstall.py |
33 | Mostly refusals: a working copy's source survives, the home directory and filesystem root are rejected, targets outside the install root are skipped, every wrong spelling of the confirmation deletes nothing; plus multi-install discovery, deleting a foreign root never shuts this server down, and the psutil running-check degrades safely when it can't see a process |
The desktop setup page has a danger zone at the bottom with two sections: removing this install, and finding every other one on the machine. Both work identically on Windows, macOS and Linux — the backend resolves the platform-specific paths, the UI just renders whatever it finds.
Expand // remove spider_ctrl from this pc. It lists exactly what will be
deleted, with sizes, and needs you to type DELETE before the button does
anything. When it finishes, the server deletes its files and stops itself.
It works in one of two modes, chosen automatically:
| Mode | When | What goes |
|---|---|---|
| Full | The tree was created by install.sh / install.ps1 |
The whole install directory |
| Artifacts | Anything else — a git clone you work in | venv, node_modules, .next, out, config, certs only. Your source is kept. |
The installers drop a .spider-ctrl-install marker, and only a tree carrying
that marker is ever removed whole. A development checkout has no marker, so
the uninstaller will not delete it even if you ask — it clears build output
and the pairing token instead.
Expand // find other installs on this machine. This is for the case the
first section doesn't cover: you ran install.sh / install.ps1 more than
once, or ran an older version that predates the safety marker, and a copy is
just sitting on disk with nothing pointing at it.
It scans the same default location both installers write to — same relative path on every OS, resolved through the account's home directory:
| OS | Default location |
|---|---|
| macOS / Linux | ~/.spider-ctrl |
| Windows | %USERPROFILE%\.spider-ctrl |
Each result shows its size, mode, and a RUNNING badge if a server is
currently live in that tree — checked cross-platform via psutil (process
command lines and the socket listening on 8765), not lsof/netstat, so
the same code runs everywhere. Expand a result to see exactly what it would
remove, then confirm the same way as above. Deleting a different install
never stops the server answering your request — only deleting your own tree
does that.
Used a custom SPIDER_CTRL_HOME at install time? It isn't recorded anywhere
on disk, so the automatic scan can't find it — type the path into Installed
somewhere else? Check a specific path and it's checked the same way.
This install:
curl -s http://localhost:8765/uninstall/plan # see what would go
curl -s -X POST http://localhost:8765/uninstall \
-H 'Content-Type: application/json' \
-d '{"confirm":"DELETE"}' # do itEvery install on the machine:
curl -s http://localhost:8765/uninstall/scan | python3 -m json.toolA specific one, or a custom path the default scan can't see:
# macOS / Linux
curl -s "http://localhost:8765/uninstall/scan?path=$HOME/.spider-ctrl"
# Windows (PowerShell)
curl.exe -s "http://localhost:8765/uninstall/scan?path=$env:USERPROFILE\.spider-ctrl"curl -s -X POST http://localhost:8765/uninstall/remove \
-H 'Content-Type: application/json' \
-d '{"root":"/path/from/the/scan/output","confirm":"DELETE"}'All four are loopback only — a paired phone can enumerate or delete nothing on your PC.
The endpoints above need a running server. Without one, use the same guard logic directly — this refuses a working copy's source exactly like the API does, so it's safe to point at your dev checkout by accident:
cd server && venv/bin/python -c "
from utils import uninstall
print(uninstall.execute('DELETE'))
"Or delete a known install location by hand once you're sure — the app has no way to protect you here, since it isn't running:
# macOS / Linux
rm -rf ~/.spider-ctrl
# Windows (PowerShell)
Remove-Item -Recurse -Force "$env:USERPROFILE\.spider-ctrl"- Firewall rules — delete the SPIDER_CTRL Server rule by hand if you
added one (Windows Defender Firewall, or
ufw delete allow 8765/tcp) - Node and Python themselves — they were on your machine already
- On Windows, the virtualenv of the install you're deleting from — a running server can't delete the files it's executing out of; the result names the folder to remove by hand afterward. Removing a different, non-running install has no such restriction.
Start here:
cd server && venv/bin/python selftest.pynames the broken subsystem and the fix, which is faster than working through this table.
| Symptom | Solution |
|---|---|
| Connected, but the trackpad and keyboard do nothing | The OS is blocking synthetic input. See Granting Input Permission — on macOS enable your terminal app under Accessibility and restart the server |
| Keyboard shortcuts do nothing | Same cause as above — key_combo goes through the same blocked path |
| Play/pause or track skip does nothing | Fixed for macOS in this version — it now posts NSSystemDefined events instead of pyautogui virtual keys, which macOS ignores. stop has no system-wide equivalent on macOS; use pause |
| Mouse moves but shortcuts don't | Accessibility is granted; check the target app isn't running elevated (Windows) or capturing the shortcut itself |
| Phone can't reach the server | Confirm both devices are on the same network, then check the firewall — this is nearly always the firewall |
Connection failed in the app |
Is server.py running? Does http://<ip>:8765/health load in the phone's browser? |
| Phone keeps asking to pair | The token was rejected — scan the QR again, or use a fresh pairing code from the PC |
Wrong or expired code |
Codes last 10 minutes and work once. Restart the server for a new one |
| Setup page says "No Server" but it's running | Safari blocks pages from reaching localhost. Use Chrome, or scan the QR printed in the terminal |
| Want to unpair every device | Restart the server and POST to /pair/rotate from the PC, or delete server/config/pairing.json |
Server prints 127.0.0.1 |
No LAN connection detected. Connect to Wi-Fi, or read the IP from ipconfig / ifconfig |
Blank page at :8765 |
Frontend isn't built — cd frontend && npm run build |
| Vercel build fails | Root Directory must be set to frontend, not the repo root |
| Trackpad does nothing | Does the console show Client connected? On macOS, grant Accessibility permission |
Stream stuck on ESTABLISHING… |
Restart the server; if it persists: pip install --force-reinstall aiortc av |
| Stream is choppy | Drop to Low or Medium quality; 5 GHz Wi-Fi helps a lot |
Volume shows -1 |
Windows: pip install pycaw comtypes. Linux: install pulseaudio-utils for pactl |
| Unicode typing does nothing | Linux needs a clipboard tool — sudo apt install xclip |
process_kill → access denied |
That process belongs to another user or the system; needs elevation |
| Terminal command times out | Default limit is 30s — pass {"timeout": 120} for long jobs |
ModuleNotFoundError: pycaw on macOS/Linux |
This is expected — pycaw is Windows-only and gated by platform markers |
| Screen recording permission on macOS | System Settings → Privacy & Security → Screen Recording → add your Terminal or Python binary |
# Terminal 1: Frontend with hot reload on :3000
cd frontend && npm run dev
# Terminal 2: Server (restart manually after edits)
cd server && python server.pyOpen http://localhost:3000 on your desktop and connect to localhost in the connection bar. For phone testing, use your LAN IP — next dev binds all interfaces.
npx tsc --noEmit # TypeScript type check
npm run lint # ESLint
npm run build # Production static export → out/-
Write the handler — create or extend a controller in
server/controllers/. Accept**_so unexpected payload keys don't raise:class MyController: def my_action(self, param1: str = "", **_): # Your logic here return {"result": "success"}
-
Register it in the
HANDLERStable inserver/server.py:HANDLERS = { ... "my_action": my_controller.my_action, }
-
Call it from the UI with
send("my_action", {...})orsendAndWaitif you need the reply.
Threading note: Handlers run in a worker thread via
asyncio.to_thread(), so blocking calls are fine. Keep anything on the input path fast, since the trackpad shares the socket.
-
Create the component in
frontend/app/components/ -
Import and add it to the appropriate tab in
page.tsx -
Use
useWebSocket()for control messages:const { send, sendAndWait } = useWebSocket(); // Fire and forget (fast, no reply) send("my_action", { param1: "value" }); // Wait for response (slower, gets data back) const result = await sendAndWait("my_action", { param1: "value" });
Contributions are welcome! Here's how you can help:
- Fork the repository
- Create a feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
- 🔐 Authentication system (shared PIN/token)
- 🎨 Theme customization UI
- 📊 System monitoring dashboard with charts
- 🔊 Per-app volume control (Windows)
- 📱 Notification forwarding
- 🖥️ Multi-monitor support for screen streaming
- 🎮 Gamepad emulation
- 📂 File upload/download support
Does SPIDER_CTRL work over the internet?
No, by design. Connections are gated by a pairing token, but the server is still built for a trusted LAN: traffic is plaintext by default and a paired device gets a shell on your machine. Use it on your home Wi-Fi, a personal hotspot, or an isolated LAN. For access from outside, put both devices on a Tailscale network and use the Tailscale IP — that works unchanged, rather than port-forwarding this to the internet.
Is there any latency?
Over a local network, control latency is typically under 10ms — you won't notice it. Screen streaming latency depends on quality settings and Wi-Fi speed, typically 30–100ms on a 5 GHz network.
Can I use this on multiple computers?
Yes! Run the server on each computer. Each will broadcast its own UDP beacon. On your phone, connect to whichever IP you want to control.
Does it work with dual monitors?
The screen streaming captures the primary monitor. The mouse and keyboard control the full desktop across all monitors.
Can my phone and PC be on different Wi-Fi networks?
No — they must be on the same local network. The simplest fix is to create a hotspot on either device and connect the other to it.
Is my data sent to any cloud server?
No. All traffic stays on your local network. The only external requests are to Google STUN servers (for WebRTC NAT traversal) and Google Fonts (for the JetBrains Mono typeface). Neither carries your data.
Can someone on my network hack my computer through this?
If someone on your network can reach port 8765, they have full control — terminal, process manager, power controls, everything. Only run SPIDER_CTRL on networks you trust (home Wi-Fi, personal hotspot). See the Security Model section.
Why does macOS ask for permissions?
macOS requires explicit user consent for apps that control input (Accessibility) or capture the screen (Screen Recording). Grant these in System Settings → Privacy & Security for your terminal or Python binary.
Can I customize the app's appearance?
Yes! The theme is defined in frontend/app/globals.css (CSS variables) and frontend/tailwind.config.js (Tailwind tokens). Change both files and rebuild with npm run build.
MIT — use it, fork it, change it, ship it.
If you find SPIDER_CTRL useful, give it a ⭐ on GitHub!
github.com/muneebkhan08/test_mob_ctrl














{ "action": "mouse_move", "payload": { "dx": 10, "dy": -5 }, "id": "req_1" // optional — include to get a reply }